@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.
- package/dist/__tests__/coordination-memory.test.js +6 -3
- package/dist/__tests__/coordination-memory.test.js.map +1 -1
- package/dist/__tests__/coordination-mode.test.d.ts +13 -0
- package/dist/__tests__/coordination-mode.test.d.ts.map +1 -0
- package/dist/__tests__/coordination-mode.test.js +126 -0
- package/dist/__tests__/coordination-mode.test.js.map +1 -0
- package/dist/__tests__/coordination-token-multitenant.test.d.ts +11 -0
- package/dist/__tests__/coordination-token-multitenant.test.d.ts.map +1 -0
- package/dist/__tests__/coordination-token-multitenant.test.js +106 -0
- package/dist/__tests__/coordination-token-multitenant.test.js.map +1 -0
- package/dist/__tests__/coordination-token.test.js +8 -8
- package/dist/__tests__/coordination-token.test.js.map +1 -1
- package/dist/__tests__/project-config.test.js +1 -0
- package/dist/__tests__/project-config.test.js.map +1 -1
- package/dist/agent-config/coordination-rosters.generated.json +15 -0
- package/dist/agent-config/hooks/coordination-brief.sh +53 -16
- package/dist/lib/coordination-client.d.ts +19 -6
- package/dist/lib/coordination-client.d.ts.map +1 -1
- package/dist/lib/coordination-client.js +61 -11
- package/dist/lib/coordination-client.js.map +1 -1
- package/dist/lib/coordination-token.d.ts +46 -15
- package/dist/lib/coordination-token.d.ts.map +1 -1
- package/dist/lib/coordination-token.js +108 -47
- package/dist/lib/coordination-token.js.map +1 -1
- package/dist/lib/project-config.d.ts +9 -0
- package/dist/lib/project-config.d.ts.map +1 -1
- package/dist/lib/project-config.js +4 -0
- package/dist/lib/project-config.js.map +1 -1
- 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
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
|
|
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
|
-
# ──
|
|
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
|
|
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
|
|
90
|
-
echo "coordination brief: team
|
|
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:
|
|
95
|
-
# The cache filename embeds
|
|
96
|
-
#
|
|
97
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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":"AAAA;;;;;;;;GAQG;
|
|
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
|
-
*
|
|
19
|
+
* Coerce a raw mode string to a resolved value, or `undefined` when unset.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
33
|
-
|
|
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
|
-
|
|
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":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
-
|
|
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
|
|
64
|
-
* caller;
|
|
65
|
-
*
|
|
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.
|
|
125
|
-
|
|
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
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
//
|
|
143
|
-
|
|
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
|
-
//
|
|
153
|
-
//
|
|
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
|
-
|
|
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
|
|
227
|
+
return { status: 'resolved', token: fetched, project, slug, source };
|
|
167
228
|
}
|
|
168
229
|
//# sourceMappingURL=coordination-token.js.map
|