@memnexus-ai/mx-agent-cli 0.1.176 → 0.1.178

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/__tests__/coordination-memory.test.js +6 -3
  2. package/dist/__tests__/coordination-memory.test.js.map +1 -1
  3. package/dist/__tests__/coordination-mode.test.d.ts +13 -0
  4. package/dist/__tests__/coordination-mode.test.d.ts.map +1 -0
  5. package/dist/__tests__/coordination-mode.test.js +126 -0
  6. package/dist/__tests__/coordination-mode.test.js.map +1 -0
  7. package/dist/__tests__/coordination-token-multitenant.test.d.ts +11 -0
  8. package/dist/__tests__/coordination-token-multitenant.test.d.ts.map +1 -0
  9. package/dist/__tests__/coordination-token-multitenant.test.js +106 -0
  10. package/dist/__tests__/coordination-token-multitenant.test.js.map +1 -0
  11. package/dist/__tests__/coordination-token.test.js +8 -8
  12. package/dist/__tests__/coordination-token.test.js.map +1 -1
  13. package/dist/__tests__/project-config.test.js +1 -0
  14. package/dist/__tests__/project-config.test.js.map +1 -1
  15. package/dist/agent-config/coordination-rosters.generated.json +15 -0
  16. package/dist/agent-config/hooks/coordination-brief.sh +53 -16
  17. package/dist/lib/coordination-client.d.ts +19 -6
  18. package/dist/lib/coordination-client.d.ts.map +1 -1
  19. package/dist/lib/coordination-client.js +61 -11
  20. package/dist/lib/coordination-client.js.map +1 -1
  21. package/dist/lib/coordination-token.d.ts +46 -15
  22. package/dist/lib/coordination-token.d.ts.map +1 -1
  23. package/dist/lib/coordination-token.js +108 -47
  24. package/dist/lib/coordination-token.js.map +1 -1
  25. package/dist/lib/project-config.d.ts +9 -0
  26. package/dist/lib/project-config.d.ts.map +1 -1
  27. package/dist/lib/project-config.js +4 -0
  28. package/dist/lib/project-config.js.map +1 -1
  29. package/package.json +1 -1
@@ -53,11 +53,19 @@ KEYVAULT_NAME="mx-dev-eastus2-kv-001"
53
53
  MIN_CLI_VERSION="0.1.172"
54
54
  CLI_UPDATE_CMD="npm install -g @memnexus-ai/mx-agent-cli@latest"
55
55
 
56
- # SF-2: canonical roster. The marker slug MUST be an EXACT member of this array
57
- # before it is ever interpolated into an `az` command. This is the real gate;
58
- # the slug regex only blocks shell metacharacters. Aliases mx-agent /
59
- # mx-agent-system are intentionally EXCLUDED (flagged for roster cleanup).
60
- COORD_ROSTER=(platform retrieval mcp pipeline marketing customer-portal eval product)
56
+ # MULTI-TENANT (Phase 1). Identity is (project, team). `project` is the committed
57
+ # project-config signal (resolveProductSlug precedence: MX_PRODUCT_SLUG
58
+ # mx-agent.config.json projectSlug memnexus). `team` is the marker slug. The
59
+ # roster is resolved from the OFFLINE generated file (ND-2) NEVER a live
60
+ # GET /roster at session start, so a coordination-service outage (the shared
61
+ # SPOF) can't break session start. For the DEFAULT project (memnexus) behavior is
62
+ # IDENTICAL to before: same roster membership, same token (via the legacy KV
63
+ # name fallback below).
64
+ DEFAULT_PROJECT="memnexus"
65
+ # BUILTIN memnexus roster — OFFLINE fallback used only when the generated file is
66
+ # unreadable (or node is unavailable). Mirrors PROVISIONED_ROSTERS in
67
+ # coordination-service/src/config/rosters.ts.
68
+ BUILTIN_MEMNEXUS_ROSTER="platform retrieval mcp pipeline marketing customer-portal eval product"
61
69
 
62
70
  # ── MF-2: authoritative marker, hook NEVER derives the slug. ─────────────────
63
71
  # The slug is an explicit assignment written into the worktree by an onboarding
@@ -80,22 +88,41 @@ if [ -z "$SLUG" ]; then
80
88
  exit 0
81
89
  fi
82
90
 
83
- # ── SF-2: roster-membership validation (exact match) before any az call. ─────
91
+ # ── Resolve the PROJECT (resolveProductSlug precedence, offline). ────────────
92
+ # 1) MX_PRODUCT_SLUG env; 2) mx-agent.config.json projectSlug at the worktree
93
+ # root; 3) default memnexus. No HTTP — reads local config only.
94
+ PROJECT="${MX_PRODUCT_SLUG:-}"
95
+ if [ -z "$PROJECT" ] && [ -f "${WORKTREE}/mx-agent.config.json" ]; then
96
+ PROJECT="$(node -e 'try{const c=require(process.argv[1]);const s=(c.projectSlug||(c.project&&c.project.slug)||"");process.stdout.write(String(s).trim())}catch(e){}' "${WORKTREE}/mx-agent.config.json" 2>/dev/null || true)"
97
+ fi
98
+ [ -z "$PROJECT" ] && PROJECT="$DEFAULT_PROJECT"
99
+
100
+ # ── SF-2: per-project roster-membership validation before any az call. ───────
101
+ # The roster is read from the OFFLINE generated file (ND-2), with the builtin
102
+ # memnexus roster as an offline fallback. NEVER a live GET /roster.
103
+ ROSTER_FILE="${MX_COORD_ROSTER_FILE:-${WORKTREE}/mx-agent-system/agent-config/coordination-rosters.generated.json}"
104
+ ROSTER_TEAMS=""
105
+ if [ -f "$ROSTER_FILE" ]; then
106
+ ROSTER_TEAMS="$(node -e 'try{const r=require(process.argv[1]);const t=(r.projects&&r.projects[process.argv[2]])||[];process.stdout.write(t.join(" "))}catch(e){}' "$ROSTER_FILE" "$PROJECT" 2>/dev/null || true)"
107
+ fi
108
+ if [ -z "$ROSTER_TEAMS" ] && [ "$PROJECT" = "$DEFAULT_PROJECT" ]; then
109
+ # Offline fallback: file missing/unparseable/node absent → builtin memnexus roster.
110
+ ROSTER_TEAMS="$BUILTIN_MEMNEXUS_ROSTER"
111
+ fi
84
112
  in_roster=""
85
- for t in "${COORD_ROSTER[@]}"; do
113
+ for t in $ROSTER_TEAMS; do
86
114
  if [ "$SLUG" = "$t" ]; then in_roster="yes"; break; fi
87
115
  done
88
116
  if [ -z "$in_roster" ]; then
89
- # Unknown/invalid slug → fail-closed. Never fetch a default token.
90
- echo "coordination brief: team slug \"$SLUG\" not in roster — skipping" >&2
117
+ # Unknown/invalid (project, team) → fail-closed. Never fetch a default token.
118
+ echo "coordination brief: (project \"$PROJECT\", team \"$SLUG\") not in roster — skipping" >&2
91
119
  exit 0
92
120
  fi
93
121
 
94
- # ── MF-1: slug-bound per-worktree token cache. ───────────────────────────────
95
- # The cache filename embeds the slug, so a worktree recycled from team A to team
96
- # B (marker rewritten to B) can NEVER reuse A's cached token: B's cache path
97
- # simply does not exist yet → refetch. A's stale file is never read.
98
- CACHE="${WORKTREE}/.mx-coordination-token-${SLUG}"
122
+ # ── MF-1: (project, team)-bound per-worktree token cache. ────────────────────
123
+ # The cache filename embeds BOTH slugs, so a worktree recycled to another
124
+ # team/project can NEVER reuse the prior tenant's cached token.
125
+ CACHE="${WORKTREE}/.mx-coordination-token--${PROJECT}--${SLUG}"
99
126
  TOKEN=""
100
127
  if [ -s "$CACHE" ]; then
101
128
  TOKEN="$(head -n1 "$CACHE" 2>/dev/null || true)"
@@ -104,11 +131,21 @@ fi
104
131
  # ── Fetch from Key Vault once if not cached (SF-3 born-0600 write). ───────────
105
132
  if [ -z "$TOKEN" ]; then
106
133
  # Fail-OPEN on any fetch problem (not logged in, no access, missing secret).
107
- # SLUG is roster-validated above and passed as a single quoted argv element.
134
+ # PROJECT/SLUG are roster-validated above; each is a single quoted argv element.
135
+ # ND-1: primary name is the parse-safe double-hyphen coordination-token--<p>--<t>.
108
136
  fetched="$(az keyvault secret show \
109
137
  --vault-name "$KEYVAULT_NAME" \
110
- --name "coordination-token-${SLUG}" \
138
+ --name "coordination-token--${PROJECT}--${SLUG}" \
111
139
  --query value -o tsv 2>/dev/null || true)"
140
+ # ZERO-ORPHAN dual-read (memnexus only): the un-migrated live vault still holds
141
+ # the legacy single-hyphen secret. Fall back to it so memnexus session-start is
142
+ # identical to today. Phase 2 renames the secret and removes this fallback.
143
+ if [ -z "$fetched" ] && [ "$PROJECT" = "$DEFAULT_PROJECT" ]; then
144
+ fetched="$(az keyvault secret show \
145
+ --vault-name "$KEYVAULT_NAME" \
146
+ --name "coordination-token-${SLUG}" \
147
+ --query value -o tsv 2>/dev/null || true)"
148
+ fi
112
149
  if [ -z "$fetched" ]; then
113
150
  # No token available — tolerate and skip (fail-open).
114
151
  exit 0
@@ -22,13 +22,26 @@ export type CoordinationMode = 'queue' | 'memory';
22
22
  /** Env override for the coordination mode (highest precedence, test seam). */
23
23
  export declare const ENV_KEY_MODE = "MX_COORDINATION_MODE";
24
24
  /**
25
- * Resolve the coordination backend mode: env config default.
25
+ * Resolve the coordination backend mode with a strict 4-level precedence, where
26
+ * each level short-circuits ONLY when it produced a non-empty value:
26
27
  *
27
- * DEFAULT is `memory` (fail-safe): nothing changes for a project until it opts
28
- * in with `mx-agent config set coordination-mode queue`. An unknown/garbage
29
- * value (typo, corrupted config) also resolves to `memory` — the mode read is
30
- * cheap and MUST NEVER crash a command, so any read failure degrades to the
31
- * safe default rather than throwing.
28
+ * 1. env `MX_COORDINATION_MODE` highest; per-invocation / test seam.
29
+ * 2. global operator config `coordination-mode` ~/.mx-agent/config.json.
30
+ * 3. committed project config `coordinationMode` — mx-agent.config.json at the
31
+ * project (worktree) root. This is the DURABLE per-project default: it is
32
+ * checked in, so a project's queue cutover survives environment rebuilds
33
+ * instead of relying on a manually-set global config value on one box.
34
+ * 4. default `memory` (fail-safe).
35
+ *
36
+ * An explicitly-set level wins even when its value is `memory` (a box-level or
37
+ * committed opt-out beats a lower default). A set-but-invalid value at any level
38
+ * resolves to `memory` and stops — it does NOT fall through. Only a truly unset
39
+ * level falls through to the next.
40
+ *
41
+ * Every read is crash-proof: the mode lookup is cheap and MUST NEVER crash a
42
+ * command, so any thrown error (unwritable HOME on config read, project-root
43
+ * walk failure, unreadable config file) degrades to the safe default rather than
44
+ * propagating.
32
45
  */
33
46
  export declare function resolveCoordinationMode(): CoordinationMode;
34
47
  export interface EnqueueRequest {
@@ -1 +1 @@
1
- {"version":3,"file":"coordination-client.d.ts","sourceRoot":"","sources":["../../src/lib/coordination-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"coordination-client.d.ts","sourceRoot":"","sources":["../../src/lib/coordination-client.ts"],"names":[],"mappings":"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"}
@@ -8,31 +8,81 @@
8
8
  * Part of PR-B / FR-7 (CLI commands) of the agent coordination queue PRD.
9
9
  */
10
10
  import { getConfigValue } from './config.js';
11
+ import { loadProjectConfig, resolveProjectConfig } from './project-config.js';
12
+ import { findProjectRoot } from './worktree.js';
11
13
  export const CONFIG_KEY_URL = 'coordination-service-url';
12
14
  export const CONFIG_KEY_TOKEN = 'coordination-service-token';
13
15
  export const CONFIG_KEY_MODE = 'coordination-mode';
14
16
  /** Env override for the coordination mode (highest precedence, test seam). */
15
17
  export const ENV_KEY_MODE = 'MX_COORDINATION_MODE';
16
18
  /**
17
- * Resolve the coordination backend mode: env config default.
19
+ * Coerce a raw mode string to a resolved value, or `undefined` when unset.
18
20
  *
19
- * DEFAULT is `memory` (fail-safe): nothing changes for a project until it opts
20
- * in with `mx-agent config set coordination-mode queue`. An unknown/garbage
21
- * value (typo, corrupted config) also resolves to `memory` the mode read is
22
- * cheap and MUST NEVER crash a command, so any read failure degrades to the
23
- * safe default rather than throwing.
21
+ * A set-but-invalid value (typo, garbage) resolves to `memory` and STOPS the
22
+ * precedence walk an explicit level, even a malformed one, is not treated as
23
+ * "unset". Only a genuinely empty/whitespace value returns `undefined` so the
24
+ * caller falls through to the next precedence level.
25
+ */
26
+ function coerceMode(raw) {
27
+ const v = raw?.trim().toLowerCase();
28
+ if (!v)
29
+ return undefined; // unset → fall through
30
+ if (v === 'queue')
31
+ return 'queue';
32
+ return 'memory'; // set-but-not-queue (incl. 'memory' and garbage) → memory, stop
33
+ }
34
+ /**
35
+ * Resolve the coordination backend mode with a strict 4-level precedence, where
36
+ * each level short-circuits ONLY when it produced a non-empty value:
37
+ *
38
+ * 1. env `MX_COORDINATION_MODE` — highest; per-invocation / test seam.
39
+ * 2. global operator config `coordination-mode` — ~/.mx-agent/config.json.
40
+ * 3. committed project config `coordinationMode` — mx-agent.config.json at the
41
+ * project (worktree) root. This is the DURABLE per-project default: it is
42
+ * checked in, so a project's queue cutover survives environment rebuilds
43
+ * instead of relying on a manually-set global config value on one box.
44
+ * 4. default `memory` (fail-safe).
45
+ *
46
+ * An explicitly-set level wins even when its value is `memory` (a box-level or
47
+ * committed opt-out beats a lower default). A set-but-invalid value at any level
48
+ * resolves to `memory` and stops — it does NOT fall through. Only a truly unset
49
+ * level falls through to the next.
50
+ *
51
+ * Every read is crash-proof: the mode lookup is cheap and MUST NEVER crash a
52
+ * command, so any thrown error (unwritable HOME on config read, project-root
53
+ * walk failure, unreadable config file) degrades to the safe default rather than
54
+ * propagating.
24
55
  */
25
56
  export function resolveCoordinationMode() {
26
- let raw;
57
+ // 1. Env override (highest).
58
+ const fromEnv = coerceMode(process.env[ENV_KEY_MODE]);
59
+ if (fromEnv)
60
+ return fromEnv;
61
+ // 2. Global operator config.
27
62
  try {
28
- raw = (process.env[ENV_KEY_MODE] || getConfigValue(CONFIG_KEY_MODE))?.trim().toLowerCase();
63
+ const fromGlobal = coerceMode(getConfigValue(CONFIG_KEY_MODE));
64
+ if (fromGlobal)
65
+ return fromGlobal;
29
66
  }
30
67
  catch {
31
68
  // getConfigValue can touch the filesystem (mkdirSync on an unwritable HOME).
32
- // Any failure safe default; never propagate.
33
- return 'memory';
69
+ // A read failure is NOT an explicit setting — fall through to project config.
70
+ }
71
+ // 3. Committed project config (durable per-project default). Reuses the same
72
+ // findProjectRoot()/resolveProjectConfig(loadProjectConfig(root)) chain as
73
+ // coordination-memory.ts, so it resolves the WORKTREE root where the
74
+ // checked-out mx-agent.config.json lives.
75
+ try {
76
+ const root = findProjectRoot();
77
+ const fromProject = coerceMode(resolveProjectConfig(loadProjectConfig(root)).coordinationMode);
78
+ if (fromProject)
79
+ return fromProject;
80
+ }
81
+ catch {
82
+ // Project-root walk or config read failed → fall through to the default.
34
83
  }
35
- return raw === 'queue' ? 'queue' : 'memory';
84
+ // 4. Fail-safe default.
85
+ return 'memory';
36
86
  }
37
87
  /** Thrown for a transport failure (service unreachable) — triggers the FR-8 fallback. */
38
88
  export class ServiceUnreachableError extends Error {
@@ -1 +1 @@
1
- {"version":3,"file":"coordination-client.js","sourceRoot":"","sources":["../../src/lib/coordination-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"coordination-client.js","sourceRoot":"","sources":["../../src/lib/coordination-client.ts"],"names":[],"mappings":"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"}
@@ -4,34 +4,54 @@
4
4
  * Ports the security-reviewed KV-fetch logic from the agent-config hook
5
5
  * `coordination-brief.sh` into the CLI so that a bare `mx-agent coordination
6
6
  * brief` (and drain/escalate/history) resolves its own token when none is
7
- * configured, instead of failing with "token not set" and falling back to the
8
- * old channel. The isolation properties the security review required are
9
- * preserved 1:1:
7
+ * configured. MULTI-TENANT (Phase 1): identity is (project, team). `project`
8
+ * comes from resolveProductSlug() (config-derived, default 'memnexus'); `team`
9
+ * from the authoritative worktree marker. The isolation properties the security
10
+ * review required are preserved 1:1, now project-aware:
10
11
  *
11
12
  * - MF-2 The team slug is read from the authoritative worktree marker
12
13
  * (.mx-coordination-team). It is NEVER derived from CLAUDE_TEAM_NAME
13
- * or the worktree name.
14
- * - SF-2 The slug is validated against the exact roster BEFORE any `az` call.
15
- * A non-member fails CLOSED (no fetch) so the caller falls back.
16
- * - MF-1 The token cache filename embeds the slug
17
- * (.mx-coordination-token-<slug>), per worktree. A worktree recycled
18
- * to a different team can never read another team's cached token.
14
+ * or the worktree name. The PROJECT is the committed project-config
15
+ * signal (resolveProductSlug), never a queue-mode client assertion.
16
+ * - SF-2 The (project, team) is validated against the per-project roster
17
+ * BEFORE any `az` call. A non-member fails CLOSED (no fetch).
18
+ * - MF-1 The token cache filename embeds BOTH slugs
19
+ * (.mx-coordination-token--<project>--<team>), per worktree. A worktree
20
+ * recycled to a different team/project can never read another tenant's
21
+ * cached token.
19
22
  * - SF-3 The cache is created born 0600 (mode on create — no readable window).
20
- * - No shell: the roster-validated slug is passed to `az` via execFile with an
23
+ * - No shell: the roster-validated slugs are passed to `az` via execFile with an
21
24
  * args array, never interpolated into a shell string.
22
- * - The token is NEVER written to the global ~/.mx-agent config (that clobbers
23
- * across worktrees — the MF-2 defect) and is NEVER logged.
25
+ * - The token is NEVER written to the global ~/.mx-agent config and NEVER logged.
24
26
  * - Fail-OPEN on infra errors (no marker, az not logged in, KV unreachable):
25
27
  * report "unprovisioned" so the caller degrades, never crash.
26
28
  * - Fail-CLOSED only on a roster mismatch.
29
+ *
30
+ * ZERO-ORPHAN KV shim (Phase 1): the secret name is the parse-safe double-hyphen
31
+ * `coordination-token--<project>--<team>` (ND-1). For the DEFAULT project
32
+ * (memnexus) the live Key Vault still holds the PRE-migration single-hyphen
33
+ * `coordination-token-<team>` secrets, so the fetch falls back to the legacy name
34
+ * when the double-hyphen name is absent (the PRD migration grace-window dual-read).
35
+ * This keeps memnexus session-start working IDENTICALLY against the un-migrated
36
+ * vault; Phase 2 renames the secrets and the fallback is removed.
27
37
  */
28
38
  /**
29
- * SF-2 canonical roster — mirror of coordination-brief.sh's COORD_ROSTER. The
30
- * marker slug MUST be an exact member of this list before it is ever passed to
31
- * `az`. Aliases (mx-agent / mx-agent-system) are intentionally excluded.
39
+ * BUILTIN memnexus provisioned-team roster — the OFFLINE fallback used only when
40
+ * the generated roster file cannot be read. Mirrors PROVISIONED_ROSTERS in
41
+ * coordination-service/src/config/rosters.ts. Keeping it here preserves
42
+ * offline-safety (a missing file never breaks the memnexus gate) without a live
43
+ * HTTP dependency. Aliases mx-agent / mx-agent-system are intentionally excluded.
32
44
  */
33
45
  export declare const COORD_ROSTER: readonly ["platform", "retrieval", "mcp", "pipeline", "marketing", "customer-portal", "eval", "product"];
34
46
  export declare const KEYVAULT_NAME = "mx-dev-eastus2-kv-001";
47
+ /** Env override for the offline roster file path (test seam / operator override). */
48
+ export declare const ENV_KEY_ROSTER_FILE = "MX_COORD_ROSTER_FILE";
49
+ /**
50
+ * ND-1 single-source KV secret name shape — reproduced here (the CLI is a
51
+ * separate package from coordination-service). MUST stay byte-identical to
52
+ * kvSecretName() in coordination-service/src/types.ts.
53
+ */
54
+ export declare function kvSecretName(project: string, team: string): string;
35
55
  export type TokenResolveStatus =
36
56
  /** A token was already in env/config; KV is skipped (hook path + manual override). */
37
57
  'already-configured'
@@ -45,6 +65,8 @@ export interface TokenResolveResult {
45
65
  status: TokenResolveStatus;
46
66
  /** The resolved token — set only for 'already-configured' and 'resolved'. */
47
67
  token?: string;
68
+ /** The resolved project (config-derived). */
69
+ project?: string;
48
70
  /** The marker slug, when one was read (roster or not). */
49
71
  slug?: string;
50
72
  /** Where the token came from — for diagnostics; never includes the token. */
@@ -64,7 +86,16 @@ export interface TokenResolveDeps {
64
86
  readFileText?: (path: string) => string | undefined;
65
87
  /** Persist the fetched token to the per-worktree cache (born 0600). */
66
88
  writeCache?: (path: string, value: string) => void;
89
+ /** Override the resolved project (test seam); defaults to resolveProductSlug(). */
90
+ project?: string;
67
91
  }
92
+ /**
93
+ * Resolve the per-project provisioned-team roster from the OFFLINE generated file
94
+ * (ND-2), falling back to the builtin roster when the file is unreadable or does
95
+ * not list the project. This is deliberately OFFLINE-SAFE: it never makes a
96
+ * session-start HTTP call to the coordination service (the shared SPOF).
97
+ */
98
+ export declare function resolveRoster(project: string, env: NodeJS.ProcessEnv, readFileText: (path: string) => string | undefined): readonly string[];
68
99
  /**
69
100
  * Resolve the coordination token, self-provisioning from Key Vault when none is
70
101
  * configured. Never throws — every failure is a typed result the caller acts on.
@@ -1 +1 @@
1
- {"version":3,"file":"coordination-token.d.ts","sourceRoot":"","sources":["../../src/lib/coordination-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAQH;;;;GAIG;AACH,eAAO,MAAM,YAAY,0GASf,CAAC;AAEX,eAAO,MAAM,aAAa,0BAA0B,CAAC;AAKrD,MAAM,MAAM,kBAAkB;AAC5B,sFAAsF;AACpF,oBAAoB;AACtB,iFAAiF;GAC/E,UAAU;AACZ,8EAA8E;GAC5E,eAAe;AACjB,+EAA+E;GAC7E,eAAe,CAAC;AAEpB,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,MAAM,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;IACjD,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,iDAAiD;IACjD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACxE,oFAAoF;IACpF,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACpD,uEAAuE;IACvE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpD;AAmDD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,GAAE,gBAAqB,GAAG,kBAAkB,CAmExF"}
1
+ {"version":3,"file":"coordination-token.d.ts","sourceRoot":"","sources":["../../src/lib/coordination-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AASH;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,0GASf,CAAC;AAMX,eAAO,MAAM,aAAa,0BAA0B,CAAC;AAErD,qFAAqF;AACrF,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAM1D;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAElE;AAYD,MAAM,MAAM,kBAAkB;AAC5B,sFAAsF;AACpF,oBAAoB;AACtB,iFAAiF;GAC/E,UAAU;AACZ,8EAA8E;GAC5E,eAAe;AACjB,+EAA+E;GAC7E,eAAe,CAAC;AAEpB,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,MAAM,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;IACjD,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,iDAAiD;IACjD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACxE,oFAAoF;IACpF,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACpD,uEAAuE;IACvE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA8CD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACjD,SAAS,MAAM,EAAE,CAkBnB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,GAAE,gBAAqB,GAAG,kBAAkB,CAqExF"}
@@ -4,36 +4,49 @@
4
4
  * Ports the security-reviewed KV-fetch logic from the agent-config hook
5
5
  * `coordination-brief.sh` into the CLI so that a bare `mx-agent coordination
6
6
  * brief` (and drain/escalate/history) resolves its own token when none is
7
- * configured, instead of failing with "token not set" and falling back to the
8
- * old channel. The isolation properties the security review required are
9
- * preserved 1:1:
7
+ * configured. MULTI-TENANT (Phase 1): identity is (project, team). `project`
8
+ * comes from resolveProductSlug() (config-derived, default 'memnexus'); `team`
9
+ * from the authoritative worktree marker. The isolation properties the security
10
+ * review required are preserved 1:1, now project-aware:
10
11
  *
11
12
  * - MF-2 The team slug is read from the authoritative worktree marker
12
13
  * (.mx-coordination-team). It is NEVER derived from CLAUDE_TEAM_NAME
13
- * or the worktree name.
14
- * - SF-2 The slug is validated against the exact roster BEFORE any `az` call.
15
- * A non-member fails CLOSED (no fetch) so the caller falls back.
16
- * - MF-1 The token cache filename embeds the slug
17
- * (.mx-coordination-token-<slug>), per worktree. A worktree recycled
18
- * to a different team can never read another team's cached token.
14
+ * or the worktree name. The PROJECT is the committed project-config
15
+ * signal (resolveProductSlug), never a queue-mode client assertion.
16
+ * - SF-2 The (project, team) is validated against the per-project roster
17
+ * BEFORE any `az` call. A non-member fails CLOSED (no fetch).
18
+ * - MF-1 The token cache filename embeds BOTH slugs
19
+ * (.mx-coordination-token--<project>--<team>), per worktree. A worktree
20
+ * recycled to a different team/project can never read another tenant's
21
+ * cached token.
19
22
  * - SF-3 The cache is created born 0600 (mode on create — no readable window).
20
- * - No shell: the roster-validated slug is passed to `az` via execFile with an
23
+ * - No shell: the roster-validated slugs are passed to `az` via execFile with an
21
24
  * args array, never interpolated into a shell string.
22
- * - The token is NEVER written to the global ~/.mx-agent config (that clobbers
23
- * across worktrees — the MF-2 defect) and is NEVER logged.
25
+ * - The token is NEVER written to the global ~/.mx-agent config and NEVER logged.
24
26
  * - Fail-OPEN on infra errors (no marker, az not logged in, KV unreachable):
25
27
  * report "unprovisioned" so the caller degrades, never crash.
26
28
  * - Fail-CLOSED only on a roster mismatch.
29
+ *
30
+ * ZERO-ORPHAN KV shim (Phase 1): the secret name is the parse-safe double-hyphen
31
+ * `coordination-token--<project>--<team>` (ND-1). For the DEFAULT project
32
+ * (memnexus) the live Key Vault still holds the PRE-migration single-hyphen
33
+ * `coordination-token-<team>` secrets, so the fetch falls back to the legacy name
34
+ * when the double-hyphen name is absent (the PRD migration grace-window dual-read).
35
+ * This keeps memnexus session-start working IDENTICALLY against the un-migrated
36
+ * vault; Phase 2 renames the secrets and the fallback is removed.
27
37
  */
28
38
  import { execFileSync } from 'node:child_process';
29
39
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
30
40
  import { join } from 'node:path';
31
41
  import { getConfigValue } from './config.js';
32
42
  import { CONFIG_KEY_TOKEN } from './coordination-client.js';
43
+ import { resolveProductSlug, DEFAULT_PRODUCT_SLUG } from './coordination-memory.js';
33
44
  /**
34
- * SF-2 canonical roster — mirror of coordination-brief.sh's COORD_ROSTER. The
35
- * marker slug MUST be an exact member of this list before it is ever passed to
36
- * `az`. Aliases (mx-agent / mx-agent-system) are intentionally excluded.
45
+ * BUILTIN memnexus provisioned-team roster — the OFFLINE fallback used only when
46
+ * the generated roster file cannot be read. Mirrors PROVISIONED_ROSTERS in
47
+ * coordination-service/src/config/rosters.ts. Keeping it here preserves
48
+ * offline-safety (a missing file never breaks the memnexus gate) without a live
49
+ * HTTP dependency. Aliases mx-agent / mx-agent-system are intentionally excluded.
37
50
  */
38
51
  export const COORD_ROSTER = [
39
52
  'platform',
@@ -45,9 +58,31 @@ export const COORD_ROSTER = [
45
58
  'eval',
46
59
  'product',
47
60
  ];
61
+ const BUILTIN_ROSTERS = {
62
+ [DEFAULT_PRODUCT_SLUG]: COORD_ROSTER,
63
+ };
48
64
  export const KEYVAULT_NAME = 'mx-dev-eastus2-kv-001';
65
+ /** Env override for the offline roster file path (test seam / operator override). */
66
+ export const ENV_KEY_ROSTER_FILE = 'MX_COORD_ROSTER_FILE';
67
+ /** Repo-relative location of the committed generated offline roster file (ND-2). */
68
+ const ROSTER_FILE_REL = join('mx-agent-system', 'agent-config', 'coordination-rosters.generated.json');
49
69
  const MARKER_FILE = '.mx-coordination-team';
50
- const CACHE_PREFIX = '.mx-coordination-token-';
70
+ /**
71
+ * ND-1 single-source KV secret name shape — reproduced here (the CLI is a
72
+ * separate package from coordination-service). MUST stay byte-identical to
73
+ * kvSecretName() in coordination-service/src/types.ts.
74
+ */
75
+ export function kvSecretName(project, team) {
76
+ return `coordination-token--${project}--${team}`;
77
+ }
78
+ /** Legacy (pre-migration) single-hyphen name — memnexus dual-read fallback only. */
79
+ function legacyKvSecretName(team) {
80
+ return `coordination-token-${team}`;
81
+ }
82
+ /** Per-worktree token cache filename, embedding BOTH slugs (MF-1). */
83
+ function cacheFileName(project, team) {
84
+ return `.mx-coordination-token--${project}--${team}`;
85
+ }
51
86
  /** Default marker/cache reader: undefined on missing or unreadable file. */
52
87
  function defaultReadFileText(path) {
53
88
  try {
@@ -60,11 +95,9 @@ function defaultReadFileText(path) {
60
95
  }
61
96
  }
62
97
  /**
63
- * Default KV fetch. The slug embedded in `secretName` is roster-validated by the
64
- * caller; it is passed to `az` as a single argv element via execFile (no shell),
65
- * so a metacharacter in a slug could never inject a command even if the roster
66
- * gate were bypassed. Any failure (az not logged in, KV unreachable, missing
67
- * secret) returns undefined → fail-open.
98
+ * Default KV fetch. The slugs embedded in `secretName` are roster-validated by
99
+ * the caller; the name is passed to `az` as a single argv element via execFile
100
+ * (no shell). Any failure returns undefined fail-open.
68
101
  */
69
102
  function defaultFetchSecret(vault, secretName) {
70
103
  try {
@@ -79,9 +112,6 @@ function defaultFetchSecret(vault, secretName) {
79
112
  /** SF-3 born-0600 cache write. A write failure is tolerated (fail-open). */
80
113
  function defaultWriteCache(path, value) {
81
114
  try {
82
- // mode 0600 on create — 0600 has no group/other bits, so no umask can widen
83
- // it. The cache is only written when absent (see resolveCoordinationToken),
84
- // so this is always a create.
85
115
  writeFileSync(path, value, { mode: 0o600 });
86
116
  }
87
117
  catch {
@@ -93,6 +123,36 @@ function firstLineNoSpace(raw) {
93
123
  const firstLine = raw.split(/\r?\n/, 1)[0] ?? '';
94
124
  return firstLine.replace(/\s+/g, '');
95
125
  }
126
+ /**
127
+ * Resolve the per-project provisioned-team roster from the OFFLINE generated file
128
+ * (ND-2), falling back to the builtin roster when the file is unreadable or does
129
+ * not list the project. This is deliberately OFFLINE-SAFE: it never makes a
130
+ * session-start HTTP call to the coordination service (the shared SPOF).
131
+ */
132
+ export function resolveRoster(project, env, readFileText) {
133
+ const candidates = [];
134
+ const override = env[ENV_KEY_ROSTER_FILE];
135
+ if (override)
136
+ candidates.push(override);
137
+ const worktree = env.CLAUDE_WORKTREE_PATH;
138
+ if (worktree)
139
+ candidates.push(join(worktree, ROSTER_FILE_REL));
140
+ for (const path of candidates) {
141
+ const raw = readFileText(path);
142
+ if (!raw)
143
+ continue;
144
+ try {
145
+ const parsed = JSON.parse(raw);
146
+ const teams = parsed.projects?.[project];
147
+ if (Array.isArray(teams))
148
+ return teams;
149
+ }
150
+ catch {
151
+ /* malformed file → fall through to the builtin fallback (offline-safe) */
152
+ }
153
+ }
154
+ return BUILTIN_ROSTERS[project] ?? [];
155
+ }
96
156
  /**
97
157
  * Resolve the coordination token, self-provisioning from Key Vault when none is
98
158
  * configured. Never throws — every failure is a typed result the caller acts on.
@@ -103,15 +163,9 @@ export function resolveCoordinationToken(deps = {}) {
103
163
  const env = deps.env ?? process.env;
104
164
  const readFileText = deps.readFileText ?? defaultReadFileText;
105
165
  // 1. Already configured → use it, skip KV. Env preferred, then global config.
106
- // This keeps the hook path (which injects MX_COORD_SERVICE_TOKEN) and any
107
- // manual `mx-agent config set coordination-service-token` working unchanged.
108
166
  const envToken = env.MX_COORD_SERVICE_TOKEN;
109
167
  if (envToken)
110
168
  return { status: 'already-configured', token: envToken, source: 'env' };
111
- // Belt-and-suspenders for the SF-A never-crash contract: getConfigValue →
112
- // loadConfig → getConfigDir calls mkdirSync, which throws on an unwritable/
113
- // read-only HOME (EACCES/EROFS/ENOSPC). Swallow it and treat the config token
114
- // as absent so resolution continues to the marker/KV path instead of throwing.
115
169
  let configToken;
116
170
  try {
117
171
  configToken = getConfigValue(CONFIG_KEY_TOKEN);
@@ -121,41 +175,48 @@ export function resolveCoordinationToken(deps = {}) {
121
175
  }
122
176
  if (configToken)
123
177
  return { status: 'already-configured', token: configToken, source: 'config' };
124
- // 2. MF-2: read the authoritative marker. No worktree path or no marker →
125
- // fail-OPEN. We do NOT derive the slug from any other signal.
178
+ // 2. Resolve the PROJECT (config-derived, default memnexus). Never throws.
179
+ const project = deps.project ?? resolveProductSlug();
180
+ // 3. MF-2: read the authoritative team marker. No worktree/marker → fail-OPEN.
126
181
  const worktree = env.CLAUDE_WORKTREE_PATH;
127
182
  if (!worktree) {
128
- return { status: 'unprovisioned', detail: 'CLAUDE_WORKTREE_PATH is unset — cannot locate the coordination marker' };
183
+ return { status: 'unprovisioned', project, detail: 'CLAUDE_WORKTREE_PATH is unset — cannot locate the coordination marker' };
129
184
  }
130
185
  const markerRaw = readFileText(join(worktree, MARKER_FILE));
131
186
  if (markerRaw === undefined) {
132
- return { status: 'unprovisioned', detail: 'no coordination marker (.mx-coordination-team) in this worktree' };
187
+ return { status: 'unprovisioned', project, detail: 'no coordination marker (.mx-coordination-team) in this worktree' };
133
188
  }
134
189
  const slug = firstLineNoSpace(markerRaw);
135
190
  if (!slug) {
136
- return { status: 'unprovisioned', detail: 'coordination marker is empty' };
191
+ return { status: 'unprovisioned', project, detail: 'coordination marker is empty' };
137
192
  }
138
- // 3. SF-2: roster-membership gate BEFORE any az call. Not a member → fail-CLOSED.
139
- if (!COORD_ROSTER.includes(slug)) {
140
- return { status: 'not-on-roster', slug };
193
+ // 4. SF-2: roster-membership gate for THIS project BEFORE any az call. The
194
+ // roster comes from the OFFLINE file (single source), builtin fallback.
195
+ const roster = resolveRoster(project, env, readFileText);
196
+ if (!roster.includes(slug)) {
197
+ return { status: 'not-on-roster', project, slug };
141
198
  }
142
- // 4. MF-1: slug-bound per-worktree cache. The slug is in the filename, so a
143
- // worktree recycled to another team (marker rewritten) can never read the
144
- // prior team's token — its cache path simply does not exist yet.
145
- const cachePath = join(worktree, `${CACHE_PREFIX}${slug}`);
199
+ // 5. MF-1: (project, team)-bound per-worktree cache.
200
+ const cachePath = join(worktree, cacheFileName(project, slug));
146
201
  const cachedRaw = readFileText(cachePath);
147
202
  if (cachedRaw) {
148
203
  const cached = firstLineNoSpace(cachedRaw);
149
204
  if (cached)
150
- return { status: 'resolved', token: cached, slug, source: 'cache' };
205
+ return { status: 'resolved', token: cached, project, slug, source: 'cache' };
151
206
  }
152
- // 5. Fetch from Key Vault once. The roster-validated slug is the only variable
153
- // part of the secret name, passed via execFile args (no shell).
207
+ // 6. Fetch from Key Vault. Primary: parse-safe double-hyphen name (ND-1).
208
+ // Fallback (memnexus only): legacy single-hyphen name the zero-orphan
209
+ // dual-read against the un-migrated live vault (Phase 2 removes it).
154
210
  const fetchSecret = deps.fetchSecret ?? defaultFetchSecret;
155
- const fetched = fetchSecret(KEYVAULT_NAME, `coordination-token-${slug}`);
211
+ let fetched = fetchSecret(KEYVAULT_NAME, kvSecretName(project, slug));
212
+ let source = 'keyvault';
213
+ if (!fetched && project === DEFAULT_PRODUCT_SLUG) {
214
+ fetched = fetchSecret(KEYVAULT_NAME, legacyKvSecretName(slug));
215
+ }
156
216
  if (!fetched) {
157
217
  return {
158
218
  status: 'unprovisioned',
219
+ project,
159
220
  slug,
160
221
  detail: 'Key Vault returned no token (az not logged in, KV unreachable, or secret missing)',
161
222
  };
@@ -163,6 +224,6 @@ export function resolveCoordinationToken(deps = {}) {
163
224
  // SF-3: cache born 0600. NEVER persisted to the global config.
164
225
  const writeCache = deps.writeCache ?? defaultWriteCache;
165
226
  writeCache(cachePath, fetched);
166
- return { status: 'resolved', token: fetched, slug, source: 'keyvault' };
227
+ return { status: 'resolved', token: fetched, project, slug, source };
167
228
  }
168
229
  //# sourceMappingURL=coordination-token.js.map