Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53e0fe912e | ||
|
|
836ec3cb1d |
@@ -100,6 +100,11 @@ steps:
|
|||||||
# repo. Pins that comment BODIES render on both paths and that a tea
|
# repo. Pins that comment BODIES render on both paths and that a tea
|
||||||
# failure is named as what it was (git-config vs credential).
|
# failure is named as what it was (git-config vs credential).
|
||||||
- bash packages/mosaic/framework/tools/git/test-issue-view-comments.sh
|
- bash packages/mosaic/framework/tools/git/test-issue-view-comments.sh
|
||||||
|
# Hermetic regression for mint-seat-credential.sh (fleet onboarding moved into
|
||||||
|
# the framework): mock curl, sandboxed brain home, no tea, no network. Pins
|
||||||
|
# that the admin seat is configured rather than hardcoded and that the seat
|
||||||
|
# slot is written from the mint response at mode 600.
|
||||||
|
- bash packages/mosaic/framework/tools/fleet/test-mint-seat-credential.sh
|
||||||
# Hermetic behavioural regression for the PreToolUse wrapper guard: proves
|
# Hermetic behavioural regression for the PreToolUse wrapper guard: proves
|
||||||
# it still blocks the three mistakes AND still lets reads, unwrapped
|
# it still blocks the three mistakes AND still lets reads, unwrapped
|
||||||
# endpoints and ordinary commands through. Both directions are asserted —
|
# endpoints and ordinary commands through. Both directions are asserted —
|
||||||
|
|||||||
@@ -24,24 +24,6 @@
|
|||||||
# $HOME points at a per-profile directory that has no credentials file.
|
# $HOME points at a per-profile directory that has no credentials file.
|
||||||
# Operators symlink /etc/mosaic/credentials.json to the host's canonical
|
# Operators symlink /etc/mosaic/credentials.json to the host's canonical
|
||||||
# file once, instead of exporting MOSAIC_CREDENTIALS_FILE per invocation.
|
# file once, instead of exporting MOSAIC_CREDENTIALS_FILE per invocation.
|
||||||
#
|
|
||||||
# GITEA SEAT SLOTS (gitea-mosaicstack / gitea-usc arms only):
|
|
||||||
# On a fleet host, a resolved git identity is a SEAT whose live credential is
|
|
||||||
# its slot file, not the shared service store. Resolution, mirroring
|
|
||||||
# get_gitea_token() in tools/git/detect-platform.sh (mosaicstack#1311 lineage):
|
|
||||||
# - MOSAIC_GIT_IDENTITY names a seat with a directory under
|
|
||||||
# ${MOSAIC_BRAIN_HOME:-~/.mosaic}/fleet/agents/<identity>/ → its token is
|
|
||||||
# read from <slot>/secrets/gitea-<instance>-<identity>.token and exported
|
|
||||||
# as GITEA_TOKEN. The URL still comes from credentials.json (it is
|
|
||||||
# provider config, not identity).
|
|
||||||
# - A seat with an EMPTY/missing slot is a REFUSAL (fail loud), not a
|
|
||||||
# fallback: there is no precedence between the seat and service stores,
|
|
||||||
# and a silent service fallback would act as the wrong identity (#1343
|
|
||||||
# family; usc/uconnect#3084 precedent).
|
|
||||||
# - No identity resolved → the service store in credentials.json, exactly
|
|
||||||
# as before. Non-fleet hosts are unchanged.
|
|
||||||
# Other services (woodpecker, authentik, ...) have no seat concept and are
|
|
||||||
# untouched by this.
|
|
||||||
|
|
||||||
if [[ -z "${MOSAIC_CREDENTIALS_FILE:-}" ]]; then
|
if [[ -z "${MOSAIC_CREDENTIALS_FILE:-}" ]]; then
|
||||||
for _cand in "$HOME/.config/mosaic/credentials.json" "/etc/mosaic/credentials.json"; do
|
for _cand in "$HOME/.config/mosaic/credentials.json" "/etc/mosaic/credentials.json"; do
|
||||||
@@ -112,35 +94,6 @@ _mosaic_load_woodpecker_legacy() {
|
|||||||
_mosaic_sync_woodpecker_env "$WOODPECKER_INSTANCE" "$WOODPECKER_URL" "$WOODPECKER_TOKEN"
|
_mosaic_sync_woodpecker_env "$WOODPECKER_INSTANCE" "$WOODPECKER_URL" "$WOODPECKER_TOKEN"
|
||||||
}
|
}
|
||||||
|
|
||||||
_gitea_seat_token() {
|
|
||||||
# Echo the seat-slot token path for $1=identity $2=instance-prefix, or rc 1
|
|
||||||
# when the identity is not a seat. Reads nothing; path logic only.
|
|
||||||
local ident="$1" pfx="$2" brain_home slot
|
|
||||||
brain_home="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
|
|
||||||
slot="$brain_home/fleet/agents/$ident/secrets/gitea-$pfx-$ident.token"
|
|
||||||
if [[ -d "$brain_home/fleet/agents/$ident" ]]; then
|
|
||||||
printf '%s' "$slot"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
_gitea_resolve_seat_or_refuse() {
|
|
||||||
# $1=identity $2=instance-prefix $3=service-name (for messages).
|
|
||||||
# Seat with a readable slot → echoes the token (caller exports).
|
|
||||||
# Seat with an empty/missing slot → rc 1 with a named refusal.
|
|
||||||
# Not a seat → rc 2 (caller falls to the service store).
|
|
||||||
local ident="$1" pfx="$2" svc="$3" slot
|
|
||||||
slot="$(_gitea_seat_token "$ident" "$pfx")" || return 2
|
|
||||||
if [[ -r "$slot" ]] && [[ -s "$slot" ]]; then
|
|
||||||
tr -d '\n' <"$slot"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo "Error: load_credentials $svc: git identity '$ident' resolves to a SEAT but its slot is empty or unreadable: $slot" >&2
|
|
||||||
echo " Refusing to fall back to the shared service store — that would act as the wrong identity. Provision the slot or unset MOSAIC_GIT_IDENTITY." >&2
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
load_credentials() {
|
load_credentials() {
|
||||||
local service="$1"
|
local service="$1"
|
||||||
|
|
||||||
@@ -230,30 +183,16 @@ EOF
|
|||||||
;;
|
;;
|
||||||
gitea-mosaicstack)
|
gitea-mosaicstack)
|
||||||
export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.mosaicstack.url')}"
|
export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.mosaicstack.url')}"
|
||||||
|
export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.mosaicstack.token')}"
|
||||||
GITEA_URL="${GITEA_URL%/}"
|
GITEA_URL="${GITEA_URL%/}"
|
||||||
[[ -n "$GITEA_URL" ]] || { echo "Error: gitea.mosaicstack.url not found" >&2; return 1; }
|
[[ -n "$GITEA_URL" ]] || { echo "Error: gitea.mosaicstack.url not found" >&2; return 1; }
|
||||||
if [[ -z "${GITEA_TOKEN:-}" && -n "${MOSAIC_GIT_IDENTITY:-}" ]]; then
|
|
||||||
local _seat_tok
|
|
||||||
_seat_tok="$(_gitea_resolve_seat_or_refuse "$MOSAIC_GIT_IDENTITY" mosaicstack gitea-mosaicstack)" \
|
|
||||||
&& export GITEA_TOKEN="$_seat_tok" && return 0
|
|
||||||
local _src_rc=$?
|
|
||||||
[[ "$_src_rc" -eq 2 ]] || return 1
|
|
||||||
fi
|
|
||||||
export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.mosaicstack.token')}"
|
|
||||||
[[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.mosaicstack.token not found" >&2; return 1; }
|
[[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.mosaicstack.token not found" >&2; return 1; }
|
||||||
;;
|
;;
|
||||||
gitea-usc)
|
gitea-usc)
|
||||||
export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.usc.url')}"
|
export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.usc.url')}"
|
||||||
|
export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.usc.token')}"
|
||||||
GITEA_URL="${GITEA_URL%/}"
|
GITEA_URL="${GITEA_URL%/}"
|
||||||
[[ -n "$GITEA_URL" ]] || { echo "Error: gitea.usc.url not found" >&2; return 1; }
|
[[ -n "$GITEA_URL" ]] || { echo "Error: gitea.usc.url not found" >&2; return 1; }
|
||||||
if [[ -z "${GITEA_TOKEN:-}" && -n "${MOSAIC_GIT_IDENTITY:-}" ]]; then
|
|
||||||
local _seat_tok
|
|
||||||
_seat_tok="$(_gitea_resolve_seat_or_refuse "$MOSAIC_GIT_IDENTITY" usc gitea-usc)" \
|
|
||||||
&& export GITEA_TOKEN="$_seat_tok" && return 0
|
|
||||||
local _src_rc=$?
|
|
||||||
[[ "$_src_rc" -eq 2 ]] || return 1
|
|
||||||
fi
|
|
||||||
export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.usc.token')}"
|
|
||||||
[[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.usc.token not found" >&2; return 1; }
|
[[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.usc.token not found" >&2; return 1; }
|
||||||
;;
|
;;
|
||||||
woodpecker-*)
|
woodpecker-*)
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Hermetic regression for load_credentials gitea seat-slot resolution.
|
|
||||||
# Sandbox brain home + sandbox credentials.json; no real credential is read.
|
|
||||||
#
|
|
||||||
# Pins:
|
|
||||||
# G1 MOSAIC_GIT_IDENTITY naming a seat with a populated slot → GITEA_TOKEN
|
|
||||||
# comes from the SLOT, URL from credentials.json.
|
|
||||||
# G2 seat with an EMPTY slot → rc 1, refusal names the identity and the
|
|
||||||
# slot path, and NO fallback to the service store occurred (the token
|
|
||||||
# must not equal the service-store value).
|
|
||||||
# G3 no identity → service store, unchanged behavior (token from
|
|
||||||
# credentials.json).
|
|
||||||
# G4 identity that is NOT a seat (no directory) → service store (same as
|
|
||||||
# G3; the identity is irrelevant on a non-fleet path).
|
|
||||||
# G5 other services are untouched: woodpecker resolution works the same
|
|
||||||
# with and without MOSAIC_GIT_IDENTITY set.
|
|
||||||
# G6 pre-existing GITEA_TOKEN env is never overridden by the seat path.
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
W="${TMPDIR:-/tmp}/creds-seat-test-$$"
|
|
||||||
BRAIN="$W/brain"; CREDS="$W/credentials.json"
|
|
||||||
mkdir -p "$BRAIN/fleet/agents/live-seat/secrets" "$BRAIN/fleet/agents/empty-seat"
|
|
||||||
printf 'seat-token-value-abc123\n' > "$BRAIN/fleet/agents/live-seat/secrets/gitea-mosaicstack-live-seat.token"
|
|
||||||
cat > "$CREDS" <<'EOF'
|
|
||||||
{"gitea":{"mosaicstack":{"url":"https://gitea.example.test","token":"service-token-value-xyz789"}},
|
|
||||||
"woodpecker":{"default":"mosaic","mosaic":{"url":"https://ci.example.test","token":"wp-token-1"}}}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
# shellcheck source=/dev/null
|
|
||||||
source "$SCRIPT_DIR/credentials.sh"
|
|
||||||
|
|
||||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
||||||
|
|
||||||
load_env() { # $1=service, $2=env-setup; clean subshell; echoes rc then token
|
|
||||||
local svc="$1" setup="$2"
|
|
||||||
(
|
|
||||||
eval "$setup"
|
|
||||||
unset GITEA_TOKEN GITEA_URL
|
|
||||||
export MOSAIC_CREDENTIALS_FILE="$CREDS" MOSAIC_BRAIN_HOME="$BRAIN"
|
|
||||||
load_credentials "$svc" >/dev/null 2>"$W/err"
|
|
||||||
rc=$?
|
|
||||||
printf '%s\n%s\n' "$rc" "${GITEA_TOKEN:-}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
# G1: seat with populated slot
|
|
||||||
out=$(load_env gitea-mosaicstack 'export MOSAIC_GIT_IDENTITY=live-seat')
|
|
||||||
rc=$(printf '%s' "$out" | sed -n 1p); tok=$(printf '%s' "$out" | sed -n 2p)
|
|
||||||
[ "$rc" = 0 ] || fail "G1: rc=$rc err=$(cat "$W/err")"
|
|
||||||
[ "$tok" = "seat-token-value-abc123" ] || fail "G1: token not from slot: ${tok:0:20}"
|
|
||||||
|
|
||||||
# G2: seat with empty slot refuses, no fallback
|
|
||||||
out=$(load_env gitea-mosaicstack 'export MOSAIC_GIT_IDENTITY=empty-seat')
|
|
||||||
rc=$(printf '%s' "$out" | sed -n 1p); tok=$(printf '%s' "$out" | sed -n 2p)
|
|
||||||
[ "$rc" = 1 ] || fail "G2: expected rc=1 refusal, got rc=$rc tok=${tok:0:20}"
|
|
||||||
[ "$tok" != "service-token-value-xyz789" ] || fail "G2: FELL BACK to service store on seat-miss"
|
|
||||||
grep -q "empty-seat" "$W/err" || fail "G2: refusal does not name the identity"
|
|
||||||
grep -q "fleet/agents/empty-seat" "$W/err" || fail "G2: refusal does not name the slot path"
|
|
||||||
|
|
||||||
# G3: no identity → service store
|
|
||||||
out=$(load_env gitea-mosaicstack 'unset MOSAIC_GIT_IDENTITY')
|
|
||||||
rc=$(printf '%s' "$out" | sed -n 1p); tok=$(printf '%s' "$out" | sed -n 2p)
|
|
||||||
[ "$rc" = 0 ] || fail "G3: rc=$rc err=$(cat "$W/err")"
|
|
||||||
[ "$tok" = "service-token-value-xyz789" ] || fail "G3: service-store token not loaded"
|
|
||||||
|
|
||||||
# G4: identity that is not a seat → service store
|
|
||||||
out=$(load_env gitea-mosaicstack 'export MOSAIC_GIT_IDENTITY=nobody')
|
|
||||||
rc=$(printf '%s' "$out" | sed -n 1p); tok=$(printf '%s' "$out" | sed -n 2p)
|
|
||||||
[ "$rc" = 0 ] || fail "G4: rc=$rc err=$(cat "$W/err")"
|
|
||||||
[ "$tok" = "service-token-value-xyz789" ] || fail "G4: non-seat identity broke the service path"
|
|
||||||
|
|
||||||
# G5: woodpecker ignores MOSAIC_GIT_IDENTITY entirely
|
|
||||||
( export MOSAIC_CREDENTIALS_FILE="$CREDS"
|
|
||||||
export MOSAIC_GIT_IDENTITY=live-seat
|
|
||||||
unset WOODPECKER_URL WOODPECKER_TOKEN
|
|
||||||
load_credentials woodpecker >/dev/null 2>&1 || fail "G5: woodpecker load failed with identity set"
|
|
||||||
[ "$WOODPECKER_TOKEN" = "wp-token-1" ] || fail "G5: woodpecker token wrong"
|
|
||||||
[ "$WOODPECKER_URL" = "https://ci.example.test" ] || fail "G5: woodpecker url wrong" )
|
|
||||||
|
|
||||||
# G6: pre-set GITEA_TOKEN env is preserved (both arms)
|
|
||||||
( export MOSAIC_CREDENTIALS_FILE="$CREDS" MOSAIC_BRAIN_HOME="$BRAIN"
|
|
||||||
export MOSAIC_GIT_IDENTITY=live-seat GITEA_TOKEN=already-set-env
|
|
||||||
load_credentials gitea-mosaicstack >/dev/null 2>&1 || fail "G6: load failed"
|
|
||||||
[ "$GITEA_TOKEN" = "already-set-env" ] || fail "G6: seat path overrode existing GITEA_TOKEN" )
|
|
||||||
|
|
||||||
rm -rf "$W"
|
|
||||||
echo "credentials seat-slot regression passed"
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Fleet tools
|
||||||
|
|
||||||
|
Seat lifecycle tools for a Mosaic fleet. Paths are relative to
|
||||||
|
`packages/mosaic/framework/tools/fleet/` (deployed to `~/.config/mosaic/tools/fleet/`).
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
| ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `start-agent-session.sh` | launch, stop, or attach a roster-driven agent session (reads `<seat>.env.generated`, honours `MOSAIC_TMUX_SOCKET`) |
|
||||||
|
| `seat-logins.sh` | project seat tokens into `tea` logins named `<instance>-<seat>` (dry-run by default, `--apply`, `--adopt`) |
|
||||||
|
| `mint-seat-credential.sh` | create the Gitea account for a seat on every configured instance, mint a token, write the seat's credential slot, then project it into `tea` |
|
||||||
|
| `start-interaction-service.sh`, `print-interaction-effective-policy.sh`, `start-tmux-holder.sh` | operator interaction service and tmux holder |
|
||||||
|
|
||||||
|
## Onboarding a seat's credential
|
||||||
|
|
||||||
|
```
|
||||||
|
MOSAIC_ADMIN_SEAT=<admin-seat> MOSAIC_SEAT_EMAIL_DOMAIN=<domain> mint-seat-credential.sh <seat>
|
||||||
|
```
|
||||||
|
|
||||||
|
- The admin token is read from `$MOSAIC_BRAIN_HOME/fleet/agents/<admin-seat>/secrets/gitea-<instance>-<admin-seat>.token`. It is never printed.
|
||||||
|
- `MOSAIC_SEAT_EMAIL_DOMAIN` is required (no default): the framework ships no estate-specific domain.
|
||||||
|
- Instances default to the map shared with `seat-logins.sh`; `MOSAIC_GITEA_INSTANCES="a b"` limits the set and `MOSAIC_GITEA_URL_<INSTANCE>` overrides a server URL (hyphens in the instance name become underscores in the variable, as in `seat-logins.sh`).
|
||||||
|
- The seat slot is written from the mint response: `.token`, `.scopes` (what was granted), `.principal`, each mode 600.
|
||||||
|
- `tea` absent is a warning, not a failure: REST-path wrappers work with the token alone.
|
||||||
|
- Regression suite: `test-mint-seat-credential.sh` (hermetic, mock curl, no network).
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# mint-seat-credential.sh — create the Gitea account and mint a token for one seat,
|
||||||
|
# on every configured instance, writing the result into that seat's credential slot.
|
||||||
|
#
|
||||||
|
# mint-seat-credential.sh [--admin-seat <seat>] [--instances "<a> <b>"] <seat>
|
||||||
|
#
|
||||||
|
# Configuration (environment; flags win over environment):
|
||||||
|
# MOSAIC_ADMIN_SEAT seat whose admin token is used to call the Gitea
|
||||||
|
# admin API. Required. Its token is read from
|
||||||
|
# $MOSAIC_BRAIN_HOME/fleet/agents/<admin>/secrets/
|
||||||
|
# gitea-<instance>-<admin>.token. Never printed.
|
||||||
|
# MOSAIC_GITEA_INSTANCES space-separated instance names to mint on.
|
||||||
|
# Default: every instance in the map below.
|
||||||
|
# MOSAIC_GITEA_URL_<INSTANCE> server URL override per instance (same
|
||||||
|
# convention as seat-logins.sh).
|
||||||
|
# MOSAIC_SEAT_EMAIL_DOMAIN domain for the account email (<seat>@<domain>).
|
||||||
|
# Required, no default: the framework tree
|
||||||
|
# carries no estate-specific domain
|
||||||
|
# (framework-PR firewall; the instance host
|
||||||
|
# map stays per seat-logins.sh precedent).
|
||||||
|
# MOSAIC_BRAIN_HOME brain checkout; default ~/.mosaic.
|
||||||
|
#
|
||||||
|
# Exit codes: 0 minted and projected on every instance; 1 at least one instance
|
||||||
|
# failed (the others are untouched or complete); 3 usage error.
|
||||||
|
#
|
||||||
|
# WHY BASIC AUTH, WHICH LOOKS WRONG AT FIRST
|
||||||
|
# Gitea refuses token auth on POST /users/{user}/tokens by design, and the Sudo
|
||||||
|
# header and sudo query parameter are both rejected there (probed 2026-08-19, probe
|
||||||
|
# token deleted). So minting for another account needs a password: this script
|
||||||
|
# generates a random one, uses it once, and never stores or prints it. Agents
|
||||||
|
# authenticate by token; the password is not a credential anyone keeps.
|
||||||
|
#
|
||||||
|
# The .scopes file is written from the mint RESPONSE rather than from what was
|
||||||
|
# requested, so the record is what was granted rather than what was asked for.
|
||||||
|
#
|
||||||
|
# SECRETS NEVER TOUCH ARGV (#1343 class, rev-security-01 review 259): the admin
|
||||||
|
# token, the generated password, and the minted seat token all pass through
|
||||||
|
# 0600 curl --config / --data files — the landed in-tree standard
|
||||||
|
# (gitea_write_auth_config in detect-platform.sh). argv is world-readable via
|
||||||
|
# /proc/<pid>/cmdline for the life of each request, and a bash -x trace would
|
||||||
|
# print every secret otherwise. The staging files are unlinked after each use.
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# Stage secrets into 0600 files; nothing secret reaches argv or a trace.
|
||||||
|
# write_auth_config <token> -> curl --config carrying the Authorization header
|
||||||
|
# (same shape as gitea_write_auth_config in
|
||||||
|
# detect-platform.sh, local so this script stays
|
||||||
|
# standalone under tools/fleet).
|
||||||
|
# write_user_config <u> <pw> -> curl --config with `user =` (covers -u).
|
||||||
|
# write_body <json> -> 0600 file for --data @file.
|
||||||
|
write_auth_config() {
|
||||||
|
local f; f=$(mktemp "${TMPDIR:-/tmp}/mosaic-mint-auth.XXXXXX") || return 1
|
||||||
|
printf 'header = "Authorization: token %s"\n' "$1" >"$f" || { rm -f "$f"; return 1; }
|
||||||
|
chmod 600 "$f"; printf '%s' "$f"
|
||||||
|
}
|
||||||
|
write_user_config() {
|
||||||
|
local f; f=$(mktemp "${TMPDIR:-/tmp}/mosaic-mint-user.XXXXXX") || return 1
|
||||||
|
printf 'user = "%s:%s"\n' "$1" "$2" >"$f" || { rm -f "$f"; return 1; }
|
||||||
|
chmod 600 "$f"; printf '%s' "$f"
|
||||||
|
}
|
||||||
|
write_body() {
|
||||||
|
local f; f=$(mktemp "${TMPDIR:-/tmp}/mosaic-mint-body.XXXXXX") || return 1
|
||||||
|
printf '%s' "$1" >"$f" || { rm -f "$f"; return 1; }
|
||||||
|
chmod 600 "$f"; printf '%s' "$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BRAIN="${MOSAIC_BRAIN_HOME:-$HOME/.mosaic}"
|
||||||
|
ADMIN="${MOSAIC_ADMIN_SEAT:-}"
|
||||||
|
INSTANCES="${MOSAIC_GITEA_INSTANCES:-}"
|
||||||
|
EMAIL_DOMAIN="${MOSAIC_SEAT_EMAIL_DOMAIN:-}"
|
||||||
|
SEAT=""
|
||||||
|
|
||||||
|
usage() { sed -n '2,20p' "${BASH_SOURCE[0]}" >&2; exit 3; }
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--admin-seat) ADMIN="${2:-}"; shift 2 ;;
|
||||||
|
--instances) INSTANCES="${2:-}"; shift 2 ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
-*) echo "mint: unknown flag: $1" >&2; exit 3 ;;
|
||||||
|
*) [[ -z "$SEAT" ]] || { echo "mint: one seat only" >&2; exit 3; }; SEAT="$1"; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$SEAT" ]] || usage
|
||||||
|
[[ "$SEAT" =~ ^[a-z0-9][a-z0-9-]*$ ]] || { echo "mint: bad seat name: $SEAT" >&2; exit 3; }
|
||||||
|
[[ -n "$ADMIN" ]] || { echo "mint: no admin seat. Set MOSAIC_ADMIN_SEAT or pass --admin-seat." >&2; exit 3; }
|
||||||
|
[[ "$ADMIN" =~ ^[a-z0-9][a-z0-9-]*$ ]] || { echo "mint: bad admin seat name: $ADMIN" >&2; exit 3; }
|
||||||
|
[[ -n "$EMAIL_DOMAIN" ]] || { echo "mint: no email domain. Set MOSAIC_SEAT_EMAIL_DOMAIN (the framework ships no estate default)." >&2; exit 3; }
|
||||||
|
|
||||||
|
# Instance -> server URL. Same map and override convention as seat-logins.sh:
|
||||||
|
# hyphens in instance names map to underscores in the override variable
|
||||||
|
# (MOSAIC_GITEA_URL_MY-INST is not a valid shell name; MY_INST is).
|
||||||
|
url_override_var() { printf 'MOSAIC_GITEA_URL_%s' "$(printf '%s' "$1" | tr '[:lower:]-' '[:upper:]_')"; }
|
||||||
|
declare -A INSTANCE_URL=(
|
||||||
|
[mosaicstack]="https://git.mosaicstack.dev"
|
||||||
|
[usc]="https://git.uscllc.com"
|
||||||
|
)
|
||||||
|
for inst in "${!INSTANCE_URL[@]}"; do
|
||||||
|
ov="$(url_override_var "$inst")"
|
||||||
|
[[ -n "${!ov:-}" ]] && INSTANCE_URL[$inst]="${!ov}"
|
||||||
|
done
|
||||||
|
[[ -n "$INSTANCES" ]] || INSTANCES="$(printf '%s\n' "${!INSTANCE_URL[@]}" | sort | tr '\n' ' ')"
|
||||||
|
|
||||||
|
SCOPES='["read:user","write:repository","write:issue","read:organization"]'
|
||||||
|
D="$BRAIN/fleet/agents/$SEAT/secrets"
|
||||||
|
mkdir -p "$D"; chmod 700 "$D"
|
||||||
|
|
||||||
|
rc=0
|
||||||
|
for KEY in $INSTANCES; do
|
||||||
|
ov="$(url_override_var "$KEY")"
|
||||||
|
BASE="${INSTANCE_URL[$KEY]:-${!ov:-}}"
|
||||||
|
[[ -n "$BASE" ]] || { echo " $KEY: no URL known for this instance (set $ov), skipped" >&2; rc=1; continue; }
|
||||||
|
ADMIN_TOKEN_FILE="$BRAIN/fleet/agents/$ADMIN/secrets/gitea-$KEY-$ADMIN.token"
|
||||||
|
[[ -r "$ADMIN_TOKEN_FILE" ]] || { echo " $KEY: no admin token for seat '$ADMIN' ($ADMIN_TOKEN_FILE), skipped" >&2; rc=1; continue; }
|
||||||
|
T="$(cat "$ADMIN_TOKEN_FILE")"
|
||||||
|
AUTH_CFG="$(write_auth_config "$T")"
|
||||||
|
PW="$(openssl rand -base64 33 | tr -d '\n/+=' | head -c 32)"
|
||||||
|
USER_CFG="$(write_user_config "$SEAT" "$PW")"
|
||||||
|
|
||||||
|
if curl -sf -o /dev/null --config "$AUTH_CFG" "$BASE/api/v1/users/$SEAT"; then
|
||||||
|
BODY="$(write_body "{\"login_name\":\"$SEAT\",\"source_id\":0,\"password\":\"$PW\",\"must_change_password\":false}")"
|
||||||
|
curl -s -o /dev/null -X PATCH -H "Content-Type: application/json" \
|
||||||
|
--config "$AUTH_CFG" --data "@$BODY" \
|
||||||
|
"$BASE/api/v1/admin/users/$SEAT"
|
||||||
|
rm -f "$BODY"; BODY=""
|
||||||
|
act="reset-pw"
|
||||||
|
else
|
||||||
|
BODY="$(write_body "{\"username\":\"$SEAT\",\"email\":\"$SEAT@$EMAIL_DOMAIN\",\"password\":\"$PW\",\"must_change_password\":false,\"full_name\":\"Mosaic fleet seat $SEAT\"}")"
|
||||||
|
curl -s -o /dev/null -X POST -H "Content-Type: application/json" \
|
||||||
|
--config "$AUTH_CFG" --data "@$BODY" \
|
||||||
|
"$BASE/api/v1/admin/users"
|
||||||
|
rm -f "$BODY"; BODY=""
|
||||||
|
act="create"
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp="$(mktemp)"; chmod 600 "$tmp"
|
||||||
|
MINT_BODY="$(write_body "{\"name\":\"mosaic-seat\",\"scopes\":$SCOPES}")"
|
||||||
|
code="$(curl -s -o "$tmp" -w '%{http_code}' -X POST -H "Content-Type: application/json" \
|
||||||
|
--config "$USER_CFG" --data "@$MINT_BODY" "$BASE/api/v1/users/$SEAT/tokens")"
|
||||||
|
rm -f "$MINT_BODY"; MINT_BODY=""
|
||||||
|
if [[ "$code" != "201" ]]; then
|
||||||
|
echo " $KEY: mint FAILED http=$code ($act)" >&2; rm -f "$tmp"; rc=1; PW=""; rm -f "$AUTH_CFG" "$USER_CFG"; continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 - "$tmp" "$D" "$KEY" "$SEAT" <<'PY'
|
||||||
|
import json,sys,pathlib
|
||||||
|
tmp,d,key,seat=sys.argv[1:5]
|
||||||
|
t=json.load(open(tmp))
|
||||||
|
p=pathlib.Path(d)
|
||||||
|
(p/f"gitea-{key}-{seat}.token").write_text(t["sha1"]+"\n")
|
||||||
|
(p/f"gitea-{key}-{seat}.scopes").write_text(json.dumps(t.get("scopes",[]))+"\n")
|
||||||
|
(p/f"gitea-{key}-{seat}.principal").write_text(seat+"\n")
|
||||||
|
for suf in ("token","scopes","principal"):
|
||||||
|
(p/f"gitea-{key}-{seat}.{suf}").chmod(0o600)
|
||||||
|
PY
|
||||||
|
rm -f "$tmp"; PW=""; rm -f "$AUTH_CFG" "$USER_CFG"
|
||||||
|
|
||||||
|
VERIFY_CFG="$(write_auth_config "$(cat "$D/gitea-$KEY-$SEAT.token")")"
|
||||||
|
login="$(curl -s --config "$VERIFY_CFG" "$BASE/api/v1/user" \
|
||||||
|
| python3 -c 'import json,sys;print(json.load(sys.stdin).get("login","ERR"))' 2>/dev/null || echo ERR)"
|
||||||
|
rm -f "$VERIFY_CFG"
|
||||||
|
if [[ "$login" == "$SEAT" ]]; then
|
||||||
|
echo " $KEY: $act, minted, GET /user -> $login"
|
||||||
|
else
|
||||||
|
echo " $KEY: minted but identity check returned '$login', expected '$SEAT'" >&2; rc=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Project into tea ─────────────────────────────────────────────────────────
|
||||||
|
# A token in the secrets dir is only half a credential. tea 0.14.0 cannot read
|
||||||
|
# that store, it only uses logins already in its own config, so a seat minted
|
||||||
|
# but not projected holds a working token and no login. Minting and projecting
|
||||||
|
# are therefore ONE operation.
|
||||||
|
#
|
||||||
|
# --adopt is deliberately NOT passed. Adopting deletes an operator-made login,
|
||||||
|
# which is a human decision. A collision reports BLOCK and a nonzero rc instead.
|
||||||
|
#
|
||||||
|
# tea absent is not a minting failure. The REST-path wrappers still work with
|
||||||
|
# the token that was just written, so warn and carry on.
|
||||||
|
SEAT_LOGINS="$SCRIPT_DIR/seat-logins.sh"
|
||||||
|
if [[ "$rc" -eq 0 ]]; then
|
||||||
|
if command -v tea >/dev/null 2>&1; then
|
||||||
|
if "$SEAT_LOGINS" --apply --seat "$SEAT"; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
echo " projection FAILED: token is minted and valid, but no tea login exists for $SEAT." >&2
|
||||||
|
echo " tea-path wrappers will not act as this seat. Re-run:" >&2
|
||||||
|
echo " $SEAT_LOGINS --apply --seat $SEAT" >&2
|
||||||
|
rc=1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " tea not on PATH: token minted, no login projected (REST-path wrappers still work)." >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $rc
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Hermetic regression for mint-seat-credential.sh: mock curl on PATH, sandboxed
|
||||||
|
# brain home, no tea, no network, no real credentials.
|
||||||
|
#
|
||||||
|
# Pins:
|
||||||
|
# M1 the seat slot is written from the mint RESPONSE (token, granted scopes,
|
||||||
|
# principal), each file mode 600, and the identity check passes.
|
||||||
|
# M2 the admin token is read from MOSAIC_ADMIN_SEAT's slot, never hardcoded;
|
||||||
|
# a missing admin token is reported per instance and exits nonzero.
|
||||||
|
# M3 MOSAIC_GITEA_INSTANCES limits which instances are touched, and the URL
|
||||||
|
# override MOSAIC_GITEA_URL_<INSTANCE> is honoured.
|
||||||
|
# M4 no admin seat configured is a usage error (rc=3), nothing written.
|
||||||
|
# M5 the admin token value never appears on stdout or stderr.
|
||||||
|
# M6 secrets never touch argv: no Authorization header, no -u user:pass, no
|
||||||
|
# inline --data JSON carrying the password, on any curl invocation; auth
|
||||||
|
# travels in --config files and bodies in --data @files (#1343 class,
|
||||||
|
# rev-security-01 review 259 blocker).
|
||||||
|
# M7 the scopes record discriminates: a requested-but-not-granted scope
|
||||||
|
# (write:issue) must be ABSENT from .scopes — the pin is on the RESPONSE,
|
||||||
|
# and a mutant writing the requested set fails here (both reviewers).
|
||||||
|
# M8 hyphenated instance names resolve their override through the underscore
|
||||||
|
# variable, matching seat-logins.sh (SF3).
|
||||||
|
# M9 MOSAIC_SEAT_EMAIL_DOMAIN is required: unset is a usage error (rc=3),
|
||||||
|
# nothing written (framework-PR firewall answer).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
WORK_ROOT="${AGENT_WORK_ROOT:-${TMPDIR:-/tmp}}"
|
||||||
|
SANDBOX="$WORK_ROOT/mint-seat-credential-test-$$"
|
||||||
|
MOCK_BIN="$SANDBOX/bin"; BRAIN="$SANDBOX/brain"; CALLS="$SANDBOX/calls.log"
|
||||||
|
cleanup() { rm -rf "$SANDBOX"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
fail() { echo "FAIL: $*"; exit 1; }
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
TARGET="$SCRIPT_DIR/mint-seat-credential.sh"
|
||||||
|
[ -f "$TARGET" ] || fail "mint-seat-credential.sh not found beside this test"
|
||||||
|
|
||||||
|
mkdir -p "$MOCK_BIN" "$BRAIN/fleet/agents/admin-seat/secrets" || fail "setup: sandbox"
|
||||||
|
: > "$CALLS"
|
||||||
|
ADMIN_TOKEN_VALUE="admin-token-value-sentinel-4491"
|
||||||
|
printf '%s\n' "$ADMIN_TOKEN_VALUE" > "$BRAIN/fleet/agents/admin-seat/secrets/gitea-alpha-admin-seat.token"
|
||||||
|
chmod 600 "$BRAIN/fleet/agents/admin-seat/secrets/gitea-alpha-admin-seat.token"
|
||||||
|
|
||||||
|
# A PATH with only the mock bin plus the system tools the script needs, and no tea.
|
||||||
|
SYS_BIN="$SANDBOX/sys"; mkdir -p "$SYS_BIN"
|
||||||
|
for t in bash sed cat mktemp openssl tr head python3 sort printf chmod mkdir rm dirname grep stat; do
|
||||||
|
p="$(command -v "$t" 2>/dev/null || true)"; [ -n "$p" ] && ln -s "$p" "$SYS_BIN/$t"
|
||||||
|
done
|
||||||
|
export PATH="$MOCK_BIN:$SYS_BIN" CALLS
|
||||||
|
export MOSAIC_BRAIN_HOME="$BRAIN"
|
||||||
|
export MOSAIC_GITEA_URL_ALPHA="https://alpha.example.test"
|
||||||
|
export MOSAIC_SEAT_EMAIL_DOMAIN="seats.example.test"
|
||||||
|
unset MOSAIC_ADMIN_SEAT MOSAIC_GITEA_INSTANCES
|
||||||
|
|
||||||
|
# --- mock curl: records method + URL + a REDACTED auth marker, answers minting --
|
||||||
|
cat > "$MOCK_BIN/curl" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
method=GET; url=""; out=""; wcode=0; auth=""; body=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-X) method="$2"; shift 2 ;;
|
||||||
|
-o) out="$2"; shift 2 ;;
|
||||||
|
-w) wcode=1; shift 2 ;;
|
||||||
|
--config)
|
||||||
|
if grep -q 'Authorization: token' "$2" 2>/dev/null; then auth="${auth}token,"; fi
|
||||||
|
if grep -q '^user = ' "$2" 2>/dev/null; then auth="${auth}user,"; fi
|
||||||
|
shift 2 ;;
|
||||||
|
--data)
|
||||||
|
case "$2" in
|
||||||
|
@*) body="@file" ;;
|
||||||
|
*) body="inline" ;;
|
||||||
|
esac
|
||||||
|
shift 2 ;;
|
||||||
|
-H|-u) shift 2 ;;
|
||||||
|
http*) url="$1"; shift ;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
printf '%s %s auth=%s body=%s\n' "$method" "$url" "${auth:-NONE}" "$body" >> "$CALLS"
|
||||||
|
emit() { if [ -n "$out" ]; then printf '%s' "$1" > "$out"; else printf '%s' "$1"; fi; }
|
||||||
|
case "$method $url" in
|
||||||
|
"GET "*/api/v1/users/newseat) exit 22 ;; # 404 under -f: account does not exist yet
|
||||||
|
"POST "*/api/v1/admin/users) emit '{}'; exit 0 ;;
|
||||||
|
"POST "*/api/v1/users/newseat/tokens) emit '{"id":9,"name":"mosaic-seat","sha1":"minted-token-7f3a","scopes":["read:user","write:repository"]}'
|
||||||
|
[ "$wcode" = 1 ] && printf '201'; exit 0 ;;
|
||||||
|
"GET "*/api/v1/user) emit '{"login":"newseat"}'; exit 0 ;;
|
||||||
|
*) emit '{}'; exit 0 ;;
|
||||||
|
esac
|
||||||
|
EOF
|
||||||
|
chmod +x "$MOCK_BIN/curl"
|
||||||
|
[ "$(command -v curl)" = "$MOCK_BIN/curl" ] || fail "setup: curl does not resolve to the mock"
|
||||||
|
command -v tea >/dev/null 2>&1 && fail "setup: tea must be absent from the sandbox PATH"
|
||||||
|
|
||||||
|
run() { bash "$TARGET" "$@" >"$SANDBOX/out" 2>"$SANDBOX/err"; echo $?; }
|
||||||
|
|
||||||
|
# M4: no admin seat configured.
|
||||||
|
rc=$(run newseat)
|
||||||
|
[ "$rc" = 3 ] || fail "M4: expected rc=3 without an admin seat, got $rc: $(cat "$SANDBOX/err")"
|
||||||
|
grep -q 'MOSAIC_ADMIN_SEAT' "$SANDBOX/err" || fail "M4: error does not name MOSAIC_ADMIN_SEAT"
|
||||||
|
[ ! -e "$BRAIN/fleet/agents/newseat/secrets/gitea-alpha-newseat.token" ] || fail "M4: a token was written without an admin seat"
|
||||||
|
|
||||||
|
# M9: email domain is required, unset is a usage error, nothing written.
|
||||||
|
rc=$(MOSAIC_ADMIN_SEAT=admin-seat MOSAIC_GITEA_INSTANCES=alpha MOSAIC_SEAT_EMAIL_DOMAIN= run newseat)
|
||||||
|
[ "$rc" = 3 ] || fail "M9: expected rc=3 with no email domain, got $rc: $(cat "$SANDBOX/err")"
|
||||||
|
grep -q 'MOSAIC_SEAT_EMAIL_DOMAIN' "$SANDBOX/err" || fail "M9: error does not name MOSAIC_SEAT_EMAIL_DOMAIN"
|
||||||
|
[ ! -s "$CALLS" ] || fail "M9: API called without an email domain"
|
||||||
|
[ ! -e "$BRAIN/fleet/agents/newseat/secrets/gitea-alpha-newseat.token" ] || fail "M9: token written without an email domain"
|
||||||
|
|
||||||
|
# M1 + M3 + M5: mint on the single configured instance.
|
||||||
|
: > "$CALLS"
|
||||||
|
rc=$(MOSAIC_ADMIN_SEAT=admin-seat MOSAIC_GITEA_INSTANCES=alpha run newseat)
|
||||||
|
[ "$rc" = 0 ] || fail "M1: expected rc=0, got $rc: $(cat "$SANDBOX/err")"
|
||||||
|
SLOT="$BRAIN/fleet/agents/newseat/secrets"
|
||||||
|
[ "$(cat "$SLOT/gitea-alpha-newseat.token")" = "minted-token-7f3a" ] || fail "M1: token file not written from the mint response"
|
||||||
|
grep -q 'write:repository' "$SLOT/gitea-alpha-newseat.scopes" || fail "M1: scopes file not written from the response"
|
||||||
|
[ "$(cat "$SLOT/gitea-alpha-newseat.principal")" = "newseat" ] || fail "M1: principal file wrong"
|
||||||
|
for suf in token scopes principal; do
|
||||||
|
m=$(stat -c '%a' "$SLOT/gitea-alpha-newseat.$suf"); [ "$m" = 600 ] || fail "M1: $suf is mode $m, expected 600"
|
||||||
|
done
|
||||||
|
grep -q 'alpha: create, minted, GET /user -> newseat' "$SANDBOX/out" || fail "M1: success line missing: $(cat "$SANDBOX/out")"
|
||||||
|
grep -q 'https://alpha.example.test/api/v1/admin/users' "$CALLS" || fail "M3: URL override not honoured: $(cat "$CALLS")"
|
||||||
|
if grep -q 'usc\|mosaicstack' "$CALLS"; then fail "M3: an instance outside MOSAIC_GITEA_INSTANCES was touched: $(cat "$CALLS")"; fi
|
||||||
|
grep -q 'tea not on PATH' "$SANDBOX/err" || fail "tea-absent path should warn, not fail: $(cat "$SANDBOX/err")"
|
||||||
|
if grep -q "$ADMIN_TOKEN_VALUE" "$SANDBOX/out" "$SANDBOX/err" "$CALLS"; then fail "M5: admin token value leaked to output or call log"; fi
|
||||||
|
|
||||||
|
# M7: scopes pin discriminates — requested-but-not-granted scope is ABSENT.
|
||||||
|
if grep -q 'write:issue' "$SLOT/gitea-alpha-newseat.scopes"; then
|
||||||
|
fail "M7: write:issue appears in .scopes — the record is the REQUESTED set, not the response"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# M6: no secret ever travels argv — every call authenticates via --config
|
||||||
|
# (token header or user= basic-auth directive) and bodies go as --data @file.
|
||||||
|
while IFS= read -r line; do
|
||||||
|
case "$line" in
|
||||||
|
*auth=NONE*) fail "M6: unauthenticated call: $line" ;;
|
||||||
|
*body=inline*) fail "M6: inline body (secret in argv risk): $line" ;;
|
||||||
|
esac
|
||||||
|
done < "$CALLS"
|
||||||
|
[ "$(grep -c 'auth=token' "$CALLS")" -eq 3 ] || fail "M6: expected exactly 3 token-auth calls (exists-check, admin write, verify), got: $(cat "$CALLS")"
|
||||||
|
grep -q 'auth=user' "$CALLS" || fail "M6: mint call did not use the user= directive: $(cat "$CALLS")"
|
||||||
|
|
||||||
|
# M8: hyphenated instance name resolves its override via the underscore variable.
|
||||||
|
printf '%s\n' "$ADMIN_TOKEN_VALUE" > "$BRAIN/fleet/agents/admin-seat/secrets/gitea-my-inst-admin-seat.token"
|
||||||
|
chmod 600 "$BRAIN/fleet/agents/admin-seat/secrets/gitea-my-inst-admin-seat.token"
|
||||||
|
export MOSAIC_GITEA_URL_MY_INST="https://myinst.example.test"
|
||||||
|
: > "$CALLS"; rm -rf "$BRAIN/fleet/agents/newseat"
|
||||||
|
rc=$(MOSAIC_ADMIN_SEAT=admin-seat MOSAIC_GITEA_INSTANCES=my-inst run newseat)
|
||||||
|
[ "$rc" = 0 ] || fail "M8: hyphenated instance mint failed rc=$rc: $(cat "$SANDBOX/err")"
|
||||||
|
grep -q 'https://myinst.example.test/api/v1/admin/users' "$CALLS" || fail "M8: hyphen override (MY_INST) not honoured: $(cat "$CALLS")"
|
||||||
|
unset MOSAIC_GITEA_URL_MY_INST
|
||||||
|
|
||||||
|
# M2: admin token missing for the instance is reported, rc=1, nothing written.
|
||||||
|
rm -rf "$BRAIN/fleet/agents/newseat"
|
||||||
|
: > "$CALLS"
|
||||||
|
rc=$(MOSAIC_ADMIN_SEAT=other-admin MOSAIC_GITEA_INSTANCES=alpha run newseat)
|
||||||
|
[ "$rc" = 1 ] || fail "M2: expected rc=1 with no admin token, got $rc"
|
||||||
|
grep -q "no admin token for seat 'other-admin'" "$SANDBOX/err" || fail "M2: missing-admin-token not reported: $(cat "$SANDBOX/err")"
|
||||||
|
[ ! -s "$CALLS" ] || fail "M2: API was called without an admin token: $(cat "$CALLS")"
|
||||||
|
[ ! -e "$BRAIN/fleet/agents/newseat/secrets/gitea-alpha-newseat.token" ] || fail "M2: token written without an admin token"
|
||||||
|
|
||||||
|
echo "mint-seat-credential regression harness passed"
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
|
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
|
||||||
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_lib/test-credentials-gitea-seats.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh"
|
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 framework/tools/quality/scripts/test-framework-drift-check.py && bash framework/tools/quality/scripts/test-framework-drift-doctor.sh && bash framework/systemd/user/test-fleet-units.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/lease-broker/revoke_noop_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-edit.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-no-status.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-fork-ci-status.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-explain-diagnostic-status-neutral.sh && bash framework/tools/git/test-detect-platform-outside-repo.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh && bash framework/tools/_scripts/test-brain-home-check.sh && bash framework/tools/fleet/test-agent-session-broker-preflight.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mosaicstack/brain": "workspace:*",
|
"@mosaicstack/brain": "workspace:*",
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export const STAGES = [
|
|||||||
'bash packages/mosaic/framework/tools/git/test-issue-close-fail-closed.sh',
|
'bash packages/mosaic/framework/tools/git/test-issue-close-fail-closed.sh',
|
||||||
'bash packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh',
|
'bash packages/mosaic/framework/tools/git/test-gitea-login-resolution.sh',
|
||||||
'bash packages/mosaic/framework/tools/git/test-issue-view-comments.sh',
|
'bash packages/mosaic/framework/tools/git/test-issue-view-comments.sh',
|
||||||
|
'bash packages/mosaic/framework/tools/fleet/test-mint-seat-credential.sh',
|
||||||
'bash packages/mosaic/framework/tools/git/test-wrapper-guard.sh',
|
'bash packages/mosaic/framework/tools/git/test-wrapper-guard.sh',
|
||||||
'bash packages/mosaic/framework/tools/git/test-mosaic-worktree-large-repo.sh',
|
'bash packages/mosaic/framework/tools/git/test-mosaic-worktree-large-repo.sh',
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user