@rubytech/create-maxy-code 0.1.405 → 0.1.406
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/package.json +1 -1
- package/payload/platform/plugins/scheduling/PLUGIN.md +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/agent-dispatch-manager-grounding.test.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/agent-dispatch-manager-grounding.test.d.ts.map +1 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/agent-dispatch-manager-grounding.test.js +19 -0
- package/payload/platform/plugins/scheduling/mcp/dist/__tests__/agent-dispatch-manager-grounding.test.js.map +1 -0
- package/payload/platform/plugins/whatsapp/mcp/dist/__tests__/deliverability-grounding.test.d.ts +2 -0
- package/payload/platform/plugins/whatsapp/mcp/dist/__tests__/deliverability-grounding.test.d.ts.map +1 -0
- package/payload/platform/plugins/whatsapp/mcp/dist/__tests__/deliverability-grounding.test.js +33 -0
- package/payload/platform/plugins/whatsapp/mcp/dist/__tests__/deliverability-grounding.test.js.map +1 -0
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +2 -0
- package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +5 -1
- package/payload/platform/services/claude-session-manager/dist/claude-host-creds.d.ts +8 -7
- package/payload/platform/services/claude-session-manager/dist/claude-host-creds.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js +63 -40
- package/payload/platform/services/claude-session-manager/dist/claude-host-creds.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.d.ts +40 -0
- package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.js +159 -0
- package/payload/platform/services/claude-session-manager/dist/claude-keychain-write.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/connector-availability.d.ts +36 -0
- package/payload/platform/services/claude-session-manager/dist/connector-availability.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/connector-availability.js +76 -0
- package/payload/platform/services/claude-session-manager/dist/connector-availability.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +2 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +2 -2
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/server/server.js +80 -63
package/package.json
CHANGED
|
@@ -129,6 +129,8 @@ An `action` invokes one pre-encoded MCP tool with no reasoning. When a due event
|
|
|
129
129
|
|
|
130
130
|
An event carries **either** an `action` **or** an `agentDispatch`, never both — setting one clears the other. Agent dispatch is stored as `agentChannel`, `agentDestination`, `agentPrompt`. Use `schedule-update` with `agentDispatch` to modify or `agentDispatch: null` to clear. On WhatsApp, a destination bound as an account-manager scopes the spawned session to its sub-account; on Telegram the session runs on the house account.
|
|
131
131
|
|
|
132
|
+
A WhatsApp `agentDispatch` destination may therefore be an account manager, not only an admin phone — this is the supported way to send a manager a proactive or scheduled WhatsApp message. The reply is delivered over the house's paired line regardless of which sub-account the session is scoped to; socket ownership never restricts which number the dispatch can reach.
|
|
133
|
+
|
|
132
134
|
Dispatch results are recorded on the Event node (`lastDispatchResult`, `lastDispatchAt`) exactly as for tool actions. Every heartbeat tick also audits stored agent dispatches and logs `[schedule-audit] op=stale-dispatch` for any whose destination has stopped being an admin or whose bound sub-account is gone, so a binding that rots between scheduling and firing is surfaced before it mis-dispatches.
|
|
133
135
|
|
|
134
136
|
## Reconciling public bookings (D1 → graph)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-dispatch-manager-grounding.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/agent-dispatch-manager-grounding.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Task 1435 — content-presence gate for the scheduling side of the WhatsApp
|
|
2
|
+
// deliverability grounding. `isValidAgentDestination` accepts an account
|
|
3
|
+
// manager (adminPhones OR accountManagers), so scheduled dispatch is the
|
|
4
|
+
// supported way to reach a manager. PLUGIN.md must say so plainly, and deliver-
|
|
5
|
+
// over-the-house-line must be stated, so the agent cites it instead of inventing
|
|
6
|
+
// a line-ownership restriction. Mirrors time-resolve-description.test.ts.
|
|
7
|
+
import { describe, it, expect } from "vitest";
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { resolve } from "path";
|
|
10
|
+
const PLUGIN = readFileSync(resolve(__dirname, "../../../PLUGIN.md"), "utf8");
|
|
11
|
+
describe("agent-dispatch account-manager grounding", () => {
|
|
12
|
+
it("PLUGIN.md states a WhatsApp dispatch destination may be an account manager", () => {
|
|
13
|
+
expect(PLUGIN).toContain("A WhatsApp `agentDispatch` destination may therefore be an account manager");
|
|
14
|
+
});
|
|
15
|
+
it("PLUGIN.md states delivery is over the house line regardless of scope", () => {
|
|
16
|
+
expect(PLUGIN).toContain("socket ownership never restricts which number the dispatch can reach");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
//# sourceMappingURL=agent-dispatch-manager-grounding.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-dispatch-manager-grounding.test.js","sourceRoot":"","sources":["../../src/__tests__/agent-dispatch-manager-grounding.test.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,yEAAyE;AACzE,yEAAyE;AACzE,gFAAgF;AAChF,iFAAiF;AACjF,0EAA0E;AAC1E,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAE/B,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,SAAS,EAAE,oBAAoB,CAAC,EAAE,MAAM,CAAC,CAAC;AAE9E,QAAQ,CAAC,0CAA0C,EAAE,GAAG,EAAE;IACxD,EAAE,CAAC,4EAA4E,EAAE,GAAG,EAAE;QACpF,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CACtB,4EAA4E,CAC7E,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CACtB,sEAAsE,CACvE,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/payload/platform/plugins/whatsapp/mcp/dist/__tests__/deliverability-grounding.test.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deliverability-grounding.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/deliverability-grounding.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Task 1435 — content-presence gate for the WhatsApp deliverability grounding.
|
|
2
|
+
//
|
|
3
|
+
// The agent confabulated a false constraint ("the single line is pointed at
|
|
4
|
+
// the wrong account so it can't message this manager") because the skill named
|
|
5
|
+
// only the `whatsapp-send-admin` admin-phones-only limit and never pointed at
|
|
6
|
+
// the scheduled-dispatch path. The routing code is correct (1377/1382/1383/1385);
|
|
7
|
+
// this gate keeps the *grounding text* that removes the confabulation's foothold
|
|
8
|
+
// from silently regressing. Mirrors scheduling's time-resolve-description.test.ts.
|
|
9
|
+
import { describe, it, expect } from "vitest";
|
|
10
|
+
import { readFileSync } from "fs";
|
|
11
|
+
import { resolve } from "path";
|
|
12
|
+
const SKILL = readFileSync(resolve(__dirname, "../../../skills/manage-whatsapp-config/SKILL.md"), "utf8");
|
|
13
|
+
const REFERENCE = readFileSync(resolve(__dirname, "../../../references/channels-whatsapp.md"), "utf8");
|
|
14
|
+
describe("WhatsApp deliverability grounding", () => {
|
|
15
|
+
it("the config skill states the no-deliverability-restriction fact", () => {
|
|
16
|
+
expect(SKILL).toContain("never restricts which numbers a message can reach");
|
|
17
|
+
expect(SKILL).toContain("Account-manager binding decides only which agent session answers");
|
|
18
|
+
});
|
|
19
|
+
it("the config skill routes proactive/scheduled manager sends to agentDispatch", () => {
|
|
20
|
+
expect(SKILL).toContain("An account manager is a valid dispatch destination");
|
|
21
|
+
expect(SKILL).toContain("`whatsapp-send-admin` (the immediate proactive-push tool) accepts admin phones only");
|
|
22
|
+
});
|
|
23
|
+
it("the config skill forbids the false line-ownership moves", () => {
|
|
24
|
+
expect(SKILL).toContain('"pointed at the wrong account"');
|
|
25
|
+
expect(SKILL).toContain("no such flip and no such restriction");
|
|
26
|
+
expect(SKILL).toContain("never for reaching an account manager");
|
|
27
|
+
});
|
|
28
|
+
it("the reference carries the explicit no-per-recipient-restriction statement", () => {
|
|
29
|
+
expect(REFERENCE).toContain("No per-recipient deliverability restriction");
|
|
30
|
+
expect(REFERENCE).toContain("socket ownership does not gate the recipient");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
//# sourceMappingURL=deliverability-grounding.test.js.map
|
package/payload/platform/plugins/whatsapp/mcp/dist/__tests__/deliverability-grounding.test.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deliverability-grounding.test.js","sourceRoot":"","sources":["../../src/__tests__/deliverability-grounding.test.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,8EAA8E;AAC9E,kFAAkF;AAClF,iFAAiF;AACjF,mFAAmF;AACnF,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAE/B,MAAM,KAAK,GAAG,YAAY,CACxB,OAAO,CAAC,SAAS,EAAE,iDAAiD,CAAC,EACrE,MAAM,CACP,CAAC;AACF,MAAM,SAAS,GAAG,YAAY,CAC5B,OAAO,CAAC,SAAS,EAAE,0CAA0C,CAAC,EAC9D,MAAM,CACP,CAAC;AAEF,QAAQ,CAAC,mCAAmC,EAAE,GAAG,EAAE;IACjD,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,mDAAmD,CAAC,CAAC;QAC7E,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CACrB,kEAAkE,CACnE,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4EAA4E,EAAE,GAAG,EAAE;QACpF,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,oDAAoD,CAAC,CAAC;QAC9E,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CACrB,qFAAqF,CACtF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,gCAAgC,CAAC,CAAC;QAC1D,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,sCAAsC,CAAC,CAAC;QAChE,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CACrB,uCAAuC,CACxC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2EAA2E,EAAE,GAAG,EAAE;QACnF,MAAM,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,6CAA6C,CAAC,CAAC;QAC3E,MAAM,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,8CAA8C,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -109,6 +109,8 @@ The platform enforces this at multiple levels:
|
|
|
109
109
|
|
|
110
110
|
**Account-manager scoping — the two-account split.** A designated manager's inbound spawns an admin session scoped to their sub-account (its graph, skills, and `${USER_ID}` attribution). Replies and file delivery still go out over the **house's** paired WhatsApp device — the only WhatsApp socket — so the sub-account scope never redirects the transport. The connection (house) account is kept for `startNativeFileFollower` and the reply path. A stale binding (a phone wrongly also on `adminPhones`) is surfaced at boot by `account-manager-drift` and never silently mis-routes.
|
|
111
111
|
|
|
112
|
+
**No per-recipient deliverability restriction.** The single paired line can deliver to any number regardless of which account owns the socket. Sub-account scope (from account-manager binding) decides which session runs the turn — its cwd, graph, and tools — and never which numbers a message can reach. To reach an account manager proactively or on a schedule, a scheduled dispatch (`agentDispatch`) accepts the manager as a valid `destination` and delivers over this same house line; there is no "point the line at the sub-account" step, because socket ownership does not gate the recipient.
|
|
113
|
+
|
|
112
114
|
**Single source of truth + fail-closed.** The effective account is resolved **exactly once**, in the gate: `checkDmAccess` returns `effectiveAccountId` (the house account for owner/`adminPhones`/public, the bound sub-account for a manager), and that value threads through the inbound payload → gateway → `ensureChannelSession`, which spawns into it **without re-reading** the `accountManagers` map. There is no second resolution and no `?? accountId` house fallback: if a manager's bound sub-account is not a valid account, the inbound is **rejected** (no session spawned, no reply), never routed to the house. This closes the escalation where a divergence between two independent map reads handed a scoped manager a house-owner admin session. Observable signals: `op=account-manager-route … effectiveAccount=… source=gate` on a routed manager inbound; `op=account-manager-reject … reason=unresolved-effective-account` on the fail-closed drop; a standing `op=escalation-tripwire` belt that can only fire if a future change reintroduces the divergence.
|
|
113
115
|
|
|
114
116
|
**The scheduler path fails closed too.** A scheduled dispatch (`POST /api/channel/schedule-inject`) reaches the same spawn machine without going through the gate, resolving its own effective account via `effectiveAccountFor`. That resolver carries the same single-source + fail-closed shape: a non-manager destination scopes to the house (an owner/admin's real scope), a valid manager scopes to the bound sub-account, and a manager whose bound sub-account is not a valid account resolves to nothing — the route **rejects** (`op=schedule-account-manager-reject … reason=unresolved-effective-account`, HTTP 403, no spawn, no reply), never routing to the house. There is no `?? accountId` fallback on this path either. Telegram scheduled dispatch is house-only by construction (no account-manager routing), so there is nothing to fail closed there.
|
|
@@ -41,7 +41,11 @@ Call `whatsapp-config action: list-account-managers` to show the current phone-t
|
|
|
41
41
|
|
|
42
42
|
A phone is a house admin or a sub-account manager, never both. Binding a phone that is already on the admin list is refused, and vice versa. To move a house admin to a sub-account, remove it from the admin list first, then add it as an account manager.
|
|
43
43
|
|
|
44
|
-
Replies to an account manager
|
|
44
|
+
Replies to an account manager go out over the house's paired WhatsApp device. That single paired line is the only WhatsApp transport, and it can deliver to any number: which account owns the socket never restricts which numbers a message can reach. Account-manager binding decides only which agent session answers an inbound or runs a scheduled turn, not deliverability.
|
|
45
|
+
|
|
46
|
+
To message a manager proactively or on a schedule, create a scheduled dispatch (`agentDispatch` on `schedule-event`) with the manager's phone as the `destination`. An account manager is a valid dispatch destination, and the turn's reply is delivered over the house's paired line. The one real limitation is that `whatsapp-send-admin` (the immediate proactive-push tool) accepts admin phones only, not managers — a narrow tool boundary, not a line-ownership problem.
|
|
47
|
+
|
|
48
|
+
Never tell the operator the line is "pointed at the wrong account", offer to point or flip the WhatsApp line at a sub-account, or describe an install-wide line flip: there is no such flip and no such restriction. Do not offer Telegram as a workaround here. Telegram is the right recommendation only for genuine Meta-policy limits (group sends, cold first-contact), never for reaching an account manager.
|
|
45
49
|
|
|
46
50
|
**Public agent:**
|
|
47
51
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* when `claudeConfigDir` is empty.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
1
|
+
import { type KeychainIO } from './claude-keychain-write.js';
|
|
2
|
+
/** Mutate `env` in place to authenticate a darwin-spawned claude, then return it.
|
|
3
|
+
* No-op on every other platform and when `claudeConfigDir` is empty. On darwin
|
|
4
|
+
* with a valid credentials file it write-through the full blob to the Keychain
|
|
5
|
+
* and skips the env token; on an unresolved item or write failure it injects the
|
|
6
|
+
* token instead. When `logger` is passed, emits one `op=spawn-hostcreds` line and
|
|
7
|
+
* one `op=connector-availability` verdict. `io` is injected for tests. */
|
|
8
|
+
export declare function applyHostCredsEnv(env: NodeJS.ProcessEnv, claudeConfigDir: string, platform?: NodeJS.Platform, logger?: (line: string) => void, io?: KeychainIO): NodeJS.ProcessEnv;
|
|
8
9
|
//# sourceMappingURL=claude-host-creds.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-host-creds.d.ts","sourceRoot":"","sources":["../src/claude-host-creds.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"claude-host-creds.d.ts","sourceRoot":"","sources":["../src/claude-host-creds.ts"],"names":[],"mappings":"AA8BA,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,4BAA4B,CAAA;AAOnC;;;;;2EAK2E;AAC3E,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,eAAe,EAAE,MAAM,EACvB,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAC/B,EAAE,GAAE,UAA6B,GAChC,MAAM,CAAC,UAAU,CAyDnB"}
|
|
@@ -1,66 +1,89 @@
|
|
|
1
|
-
// Task 1406 — macOS credential-store bridge
|
|
2
|
-
// wiring, which was inert on CLI v2.1.201).
|
|
1
|
+
// Task 1406 / 1431 — macOS credential-store bridge.
|
|
3
2
|
//
|
|
4
|
-
// On macOS the `claude` CLI
|
|
5
|
-
//
|
|
6
|
-
// (`<CLAUDE_CONFIG_DIR>/.credentials.json`,
|
|
7
|
-
// readCredentials()/writeOAuthCredentials()
|
|
8
|
-
//
|
|
9
|
-
// prints `Not logged in · Please run /login`, even though platform auth-health
|
|
10
|
-
// reads the FILE and reports `ok`.
|
|
3
|
+
// On macOS the `claude` CLI keeps its login in the login Keychain, while this
|
|
4
|
+
// platform reads and refreshes the token from a FILE
|
|
5
|
+
// (`<CLAUDE_CONFIG_DIR>/.credentials.json`, owned by the UI server's
|
|
6
|
+
// readCredentials()/writeOAuthCredentials()). Left alone the two never meet: the
|
|
7
|
+
// child authenticates against a stale/empty Keychain.
|
|
11
8
|
//
|
|
12
|
-
// Task
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// `{claudeAiOauth:{…}}` OAuth blob. The platform set neither the companion flag
|
|
18
|
-
// nor that shape, so the CLI ignored the file and fell back to the Keychain. The
|
|
19
|
-
// wiring was a no-op.
|
|
9
|
+
// Task 1406 bridged this by injecting the file's access token as
|
|
10
|
+
// CLAUDE_CODE_OAUTH_TOKEN. That restored inference, but an env token is treated
|
|
11
|
+
// as API-key precedence by the CLI (`So()` false), which disables the claude.ai
|
|
12
|
+
// account-connector sync — the darwin agent saw zero `mcp__claude_ai_*` tools
|
|
13
|
+
// (Task 1431). The child banner read `Claude API`, not the Max subscription.
|
|
20
14
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
15
|
+
// Task 1431 replaces the injection with a Keychain WRITE-THROUGH: copy the file's
|
|
16
|
+
// full claudeAiOauth blob into the exact login-Keychain item the CLI reads, and
|
|
17
|
+
// then do NOT set CLAUDE_CODE_OAUTH_TOKEN. The child resolves a real subscription
|
|
18
|
+
// login from the Keychain → inference works AND the account-connector sync runs.
|
|
19
|
+
// The host stays the primary refresh owner: auth-health refreshes the file and
|
|
20
|
+
// each spawn re-syncs the Keychain from it. (The residual mid-session self-refresh
|
|
21
|
+
// race is tracked by Tasks 1426/1427, not solved here.)
|
|
28
22
|
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
23
|
+
// When the Keychain item can't be resolved (never computing the version-coupled
|
|
24
|
+
// <hash> — Task 1398) or the write fails, the bridge falls back to the Task 1406
|
|
25
|
+
// token injection so inference never breaks; connectors stay off until a login
|
|
26
|
+
// re-establishes the item. Darwin-only; Linux reads/refreshes the same file under
|
|
27
|
+
// CLAUDE_CONFIG_DIR directly, so its env is byte-for-byte unchanged.
|
|
32
28
|
import { existsSync, readFileSync } from 'node:fs';
|
|
33
29
|
import { join } from 'node:path';
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
30
|
+
import { realKeychainIO, resolveKeychain, } from './claude-keychain-write.js';
|
|
31
|
+
import { classifyConnectorAvailability, credsHaveConnectorScope, } from './connector-availability.js';
|
|
32
|
+
/** Mutate `env` in place to authenticate a darwin-spawned claude, then return it.
|
|
33
|
+
* No-op on every other platform and when `claudeConfigDir` is empty. On darwin
|
|
34
|
+
* with a valid credentials file it write-through the full blob to the Keychain
|
|
35
|
+
* and skips the env token; on an unresolved item or write failure it injects the
|
|
36
|
+
* token instead. When `logger` is passed, emits one `op=spawn-hostcreds` line and
|
|
37
|
+
* one `op=connector-availability` verdict. `io` is injected for tests. */
|
|
38
|
+
export function applyHostCredsEnv(env, claudeConfigDir, platform = process.platform, logger, io = realKeychainIO()) {
|
|
41
39
|
if (platform !== 'darwin' || !claudeConfigDir)
|
|
42
40
|
return env;
|
|
43
41
|
const credsFile = join(claudeConfigDir, '.credentials.json');
|
|
44
42
|
const fileExists = existsSync(credsFile);
|
|
45
43
|
let expiresAt = null;
|
|
46
|
-
let
|
|
44
|
+
let accessToken = null;
|
|
45
|
+
let rawBlob = '';
|
|
47
46
|
if (fileExists) {
|
|
48
47
|
try {
|
|
49
|
-
|
|
48
|
+
rawBlob = readFileSync(credsFile, 'utf8');
|
|
49
|
+
const parsed = JSON.parse(rawBlob);
|
|
50
50
|
const oauth = parsed.claudeAiOauth;
|
|
51
51
|
if (typeof oauth?.expiresAt === 'number')
|
|
52
52
|
expiresAt = oauth.expiresAt;
|
|
53
53
|
if (typeof oauth?.accessToken === 'string' && oauth.accessToken.length > 0) {
|
|
54
|
-
|
|
55
|
-
tokenApplied = true;
|
|
54
|
+
accessToken = oauth.accessToken;
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
catch {
|
|
59
58
|
// Malformed/unreadable file: leave the child unauthenticated rather than
|
|
60
|
-
// throw and wedge the spawn.
|
|
59
|
+
// throw and wedge the spawn. mode stays 'none'; the logger records it.
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
|
-
|
|
62
|
+
let mode = 'none';
|
|
63
|
+
let keychainService = 'none';
|
|
64
|
+
if (accessToken) {
|
|
65
|
+
const resolved = resolveKeychain(claudeConfigDir, io);
|
|
66
|
+
if (resolved && io.writePassword(resolved.service, resolved.account, rawBlob)) {
|
|
67
|
+
// Write-through succeeded → the child reads a real subscription login from
|
|
68
|
+
// the Keychain. Do NOT set the env token (it would force API mode).
|
|
69
|
+
mode = 'keychain-writeback';
|
|
70
|
+
keychainService = resolved.service;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Unresolved item or failed write → preserve inference with the token.
|
|
74
|
+
env.CLAUDE_CODE_OAUTH_TOKEN = accessToken;
|
|
75
|
+
mode = 'token-fallback';
|
|
76
|
+
keychainService = resolved ? `${resolved.service}:write-failed` : 'unresolved';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
logger?.(`[spawn] op=spawn-hostcreds platform=darwin credsFile=${credsFile} fileExists=${fileExists} expiresAt=${expiresAt ?? 'none'} configDir=${claudeConfigDir} mode=${mode} keychainService=${keychainService} tokenApplied=${mode === 'token-fallback'}`);
|
|
80
|
+
// Cheap spawn-time reconciliation: connectors are expected when the file carries
|
|
81
|
+
// a scoped subscription token; if the spawn fell back to token injection the env
|
|
82
|
+
// token will suppress them (verdict=blocked-by-env-token). `loaded` is unknown at
|
|
83
|
+
// spawn — the post-condition probe is the standing audit (connector-availability).
|
|
84
|
+
const expected = credsHaveConnectorScope(rawBlob);
|
|
85
|
+
const verdict = classifyConnectorAvailability({ expected, mode, loaded: null });
|
|
86
|
+
logger?.(`[spawn] op=connector-availability phase=spawn configDir=${claudeConfigDir} expected=${expected} mode=${mode} verdict=${verdict}`);
|
|
64
87
|
return env;
|
|
65
88
|
}
|
|
66
89
|
//# sourceMappingURL=claude-host-creds.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-host-creds.js","sourceRoot":"","sources":["../src/claude-host-creds.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"claude-host-creds.js","sourceRoot":"","sources":["../src/claude-host-creds.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,EAAE;AACF,8EAA8E;AAC9E,qDAAqD;AACrD,qEAAqE;AACrE,iFAAiF;AACjF,sDAAsD;AACtD,EAAE;AACF,iEAAiE;AACjE,gFAAgF;AAChF,gFAAgF;AAChF,8EAA8E;AAC9E,6EAA6E;AAC7E,EAAE;AACF,kFAAkF;AAClF,gFAAgF;AAChF,kFAAkF;AAClF,iFAAiF;AACjF,+EAA+E;AAC/E,mFAAmF;AACnF,wDAAwD;AACxD,EAAE;AACF,gFAAgF;AAChF,iFAAiF;AACjF,+EAA+E;AAC/E,kFAAkF;AAClF,qEAAqE;AAErE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EACL,cAAc,EACd,eAAe,GAEhB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EACL,6BAA6B,EAC7B,uBAAuB,GAExB,MAAM,6BAA6B,CAAA;AAEpC;;;;;2EAK2E;AAC3E,MAAM,UAAU,iBAAiB,CAC/B,GAAsB,EACtB,eAAuB,EACvB,WAA4B,OAAO,CAAC,QAAQ,EAC5C,MAA+B,EAC/B,KAAiB,cAAc,EAAE;IAEjC,IAAI,QAAQ,KAAK,QAAQ,IAAI,CAAC,eAAe;QAAE,OAAO,GAAG,CAAA;IAEzD,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAA;IAC5D,MAAM,UAAU,GAAG,UAAU,CAAC,SAAS,CAAC,CAAA;IACxC,IAAI,SAAS,GAAkB,IAAI,CAAA;IACnC,IAAI,WAAW,GAAkB,IAAI,CAAA;IACrC,IAAI,OAAO,GAAG,EAAE,CAAA;IAChB,IAAI,UAAU,EAAE,CAAC;QACf,IAAI,CAAC;YACH,OAAO,GAAG,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;YACzC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAEhC,CAAA;YACD,MAAM,KAAK,GAAG,MAAM,CAAC,aAAa,CAAA;YAClC,IAAI,OAAO,KAAK,EAAE,SAAS,KAAK,QAAQ;gBAAE,SAAS,GAAG,KAAK,CAAC,SAAS,CAAA;YACrE,IAAI,OAAO,KAAK,EAAE,WAAW,KAAK,QAAQ,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3E,WAAW,GAAG,KAAK,CAAC,WAAW,CAAA;YACjC,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,yEAAyE;YACzE,uEAAuE;QACzE,CAAC;IACH,CAAC;IAED,IAAI,IAAI,GAAkB,MAAM,CAAA;IAChC,IAAI,eAAe,GAAG,MAAM,CAAA;IAC5B,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,QAAQ,GAAG,eAAe,CAAC,eAAe,EAAE,EAAE,CAAC,CAAA;QACrD,IAAI,QAAQ,IAAI,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;YAC9E,2EAA2E;YAC3E,oEAAoE;YACpE,IAAI,GAAG,oBAAoB,CAAA;YAC3B,eAAe,GAAG,QAAQ,CAAC,OAAO,CAAA;QACpC,CAAC;aAAM,CAAC;YACN,uEAAuE;YACvE,GAAG,CAAC,uBAAuB,GAAG,WAAW,CAAA;YACzC,IAAI,GAAG,gBAAgB,CAAA;YACvB,eAAe,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,YAAY,CAAA;QAChF,CAAC;IACH,CAAC;IAED,MAAM,EAAE,CACN,wDAAwD,SAAS,eAAe,UAAU,cAAc,SAAS,IAAI,MAAM,cAAc,eAAe,SAAS,IAAI,oBAAoB,eAAe,iBAAiB,IAAI,KAAK,gBAAgB,EAAE,CACrP,CAAA;IAED,iFAAiF;IACjF,iFAAiF;IACjF,kFAAkF;IAClF,mFAAmF;IACnF,MAAM,QAAQ,GAAG,uBAAuB,CAAC,OAAO,CAAC,CAAA;IACjD,MAAM,OAAO,GAAG,6BAA6B,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;IAC/E,MAAM,EAAE,CACN,2DAA2D,eAAe,aAAa,QAAQ,SAAS,IAAI,YAAY,OAAO,EAAE,CAClI,CAAA;IAED,OAAO,GAAG,CAAA;AACZ,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface KeychainItem {
|
|
2
|
+
service: string;
|
|
3
|
+
/** `YYYYMMDDHHMMSSZ` modified-time — lexicographically sortable. */
|
|
4
|
+
mdat: string;
|
|
5
|
+
}
|
|
6
|
+
export interface KeychainIO {
|
|
7
|
+
/** Persisted service name for this config dir, or null if none captured yet. */
|
|
8
|
+
readPersisted: (configDir: string) => string | null;
|
|
9
|
+
/** Record the resolved service name for this config dir. */
|
|
10
|
+
persist: (configDir: string, service: string) => void;
|
|
11
|
+
/** All `Claude Code-credentials*` generic-password items with their mdat. */
|
|
12
|
+
listItems: () => KeychainItem[];
|
|
13
|
+
/** The `acct` attribute of the named item, or null if the item is absent. */
|
|
14
|
+
getAccount: (service: string) => string | null;
|
|
15
|
+
/** Replace the item's password with `blob`. Returns false on any failure. */
|
|
16
|
+
writePassword: (service: string, account: string, blob: string) => boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface ResolvedKeychain {
|
|
19
|
+
service: string;
|
|
20
|
+
account: string;
|
|
21
|
+
}
|
|
22
|
+
/** Parse `security dump-keychain` output into `Claude Code-credentials*` items
|
|
23
|
+
* paired with their mdat. `mdat` precedes `svce` in each block, matching the
|
|
24
|
+
* dump layout. Pure. */
|
|
25
|
+
export declare function parseKeychainItems(dumpOutput: string): KeychainItem[];
|
|
26
|
+
/** Parse the `acct` blob from `security find-generic-password` attribute output.
|
|
27
|
+
* Returns null when no acct attribute is present. Pure. */
|
|
28
|
+
export declare function parseKeychainAccount(attrOutput: string): string | null;
|
|
29
|
+
/** Resolve the Keychain item for `configDir`, never computing the hash. The
|
|
30
|
+
* newest hashed item wins (self-heals across a hash rotation); a genuine mdat
|
|
31
|
+
* tie is broken by the persisted login-writeback name, and an unbreakable tie
|
|
32
|
+
* returns null (never guess). */
|
|
33
|
+
export declare function resolveKeychain(configDir: string, io: KeychainIO): ResolvedKeychain | null;
|
|
34
|
+
/** Real `security`/filesystem implementation of KeychainIO. Every method absorbs
|
|
35
|
+
* its own errors so a Keychain fault degrades to token-fallback rather than
|
|
36
|
+
* throwing into the spawn path. */
|
|
37
|
+
export declare function realKeychainIO(): KeychainIO;
|
|
38
|
+
/** The OS username, the `acct` the CLI stores its credentials item under. */
|
|
39
|
+
export declare function currentAccount(): string;
|
|
40
|
+
//# sourceMappingURL=claude-keychain-write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-keychain-write.d.ts","sourceRoot":"","sources":["../src/claude-keychain-write.ts"],"names":[],"mappings":"AAsCA,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,UAAU;IACzB,gFAAgF;IAChF,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;IACnD,4DAA4D;IAC5D,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACrD,6EAA6E;IAC7E,SAAS,EAAE,MAAM,YAAY,EAAE,CAAA;IAC/B,6EAA6E;IAC7E,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;IAC9C,6EAA6E;IAC7E,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;CAC3E;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;yBAEyB;AACzB,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,YAAY,EAAE,CAerE;AAED;4DAC4D;AAC5D,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMtE;AAED;;;kCAGkC;AAClC,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,GAAG,gBAAgB,GAAG,IAAI,CAoB1F;AAED;;oCAEoC;AACpC,wBAAgB,cAAc,IAAI,UAAU,CAuD3C;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,IAAI,MAAM,CAMvC"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Task 1431 — darwin keychain write-through.
|
|
2
|
+
//
|
|
3
|
+
// macOS `claude` reads its login from the login Keychain, not the file. The
|
|
4
|
+
// account-connector sync (`/v1/mcp_servers` fetch) runs only when the resolved
|
|
5
|
+
// Keychain creds carry a subscription accessToken with the `user:mcp_servers`
|
|
6
|
+
// scope AND no env auth source takes precedence. The Task 1406 bridge injected
|
|
7
|
+
// CLAUDE_CODE_OAUTH_TOKEN from the file, which restored inference but forced the
|
|
8
|
+
// CLI into API-key precedence (`So()` false) and silently disabled every
|
|
9
|
+
// claude.ai account connector. This module instead writes the file's full
|
|
10
|
+
// claudeAiOauth blob into the Keychain item the CLI reads, so the child is a real
|
|
11
|
+
// subscription login.
|
|
12
|
+
//
|
|
13
|
+
// The Keychain item is a login-keychain generic-password whose service is
|
|
14
|
+
// `Claude Code-credentials-<hash>`; the <hash> is claude-internal and
|
|
15
|
+
// version-coupled, so we NEVER compute it (Task 1398). We resolve the item by
|
|
16
|
+
// discovering the NEWEST hashed item (the one a login or a prior write-through
|
|
17
|
+
// last touched), and only ever write to an item that already exists — the
|
|
18
|
+
// service name is discovered, never fabricated. Newest-by-mdat self-heals across
|
|
19
|
+
// a CLI hash rotation: the old item lingers (macOS never deletes it) but the
|
|
20
|
+
// re-login writes a newer one, so a stale persisted name can never redirect the
|
|
21
|
+
// write to a dead item. The persisted name (captured at login-writeback) only
|
|
22
|
+
// breaks a genuine mdat tie. This targets the single-brand darwin dev runtime;
|
|
23
|
+
// two brands actively spawning on one Mac is not a supported darwin topology.
|
|
24
|
+
//
|
|
25
|
+
// Darwin-only; all callers no-op elsewhere. All `security` IO is injected so the
|
|
26
|
+
// resolver and writer are unit-tested with fakes.
|
|
27
|
+
import { execFileSync } from 'node:child_process';
|
|
28
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
29
|
+
import { userInfo } from 'node:os';
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
const CREDS_SERVICE_PREFIX = 'Claude Code-credentials';
|
|
32
|
+
/** Hashed brand variants only: `Claude Code-credentials-<suffix>`. Excludes the
|
|
33
|
+
* bare desktop item `Claude Code-credentials` (no dash suffix). */
|
|
34
|
+
const HASHED_SERVICE_RE = /^Claude Code-credentials-.+$/;
|
|
35
|
+
const PERSIST_FILENAME = '.keychain-service';
|
|
36
|
+
/** Parse `security dump-keychain` output into `Claude Code-credentials*` items
|
|
37
|
+
* paired with their mdat. `mdat` precedes `svce` in each block, matching the
|
|
38
|
+
* dump layout. Pure. */
|
|
39
|
+
export function parseKeychainItems(dumpOutput) {
|
|
40
|
+
const out = [];
|
|
41
|
+
let pendingMdat = null;
|
|
42
|
+
for (const line of dumpOutput.split('\n')) {
|
|
43
|
+
const mdatMatch = line.match(/"mdat"<timedate>=0x[0-9A-Fa-f]*\s+"([0-9]{14}Z)\\000"/);
|
|
44
|
+
if (mdatMatch) {
|
|
45
|
+
pendingMdat = mdatMatch[1];
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const svceMatch = line.match(/"svce"<blob>="(.+)"\s*$/);
|
|
49
|
+
if (svceMatch) {
|
|
50
|
+
if (svceMatch[1].startsWith(CREDS_SERVICE_PREFIX) && pendingMdat) {
|
|
51
|
+
out.push({ service: svceMatch[1], mdat: pendingMdat });
|
|
52
|
+
}
|
|
53
|
+
pendingMdat = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
/** Parse the `acct` blob from `security find-generic-password` attribute output.
|
|
59
|
+
* Returns null when no acct attribute is present. Pure. */
|
|
60
|
+
export function parseKeychainAccount(attrOutput) {
|
|
61
|
+
for (const line of attrOutput.split('\n')) {
|
|
62
|
+
const m = line.match(/"acct"<blob>="(.*)"\s*$/);
|
|
63
|
+
if (m)
|
|
64
|
+
return m[1];
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/** Resolve the Keychain item for `configDir`, never computing the hash. The
|
|
69
|
+
* newest hashed item wins (self-heals across a hash rotation); a genuine mdat
|
|
70
|
+
* tie is broken by the persisted login-writeback name, and an unbreakable tie
|
|
71
|
+
* returns null (never guess). */
|
|
72
|
+
export function resolveKeychain(configDir, io) {
|
|
73
|
+
const hashed = io.listItems().filter((i) => HASHED_SERVICE_RE.test(i.service));
|
|
74
|
+
if (hashed.length === 0)
|
|
75
|
+
return null;
|
|
76
|
+
const newestMdat = hashed.reduce((m, i) => (i.mdat > m ? i.mdat : m), hashed[0].mdat);
|
|
77
|
+
const newest = hashed.filter((i) => i.mdat === newestMdat);
|
|
78
|
+
let service;
|
|
79
|
+
if (newest.length === 1) {
|
|
80
|
+
service = newest[0].service;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
const persisted = io.readPersisted(configDir);
|
|
84
|
+
service = persisted && newest.some((i) => i.service === persisted) ? persisted : null;
|
|
85
|
+
}
|
|
86
|
+
if (!service)
|
|
87
|
+
return null;
|
|
88
|
+
const account = io.getAccount(service);
|
|
89
|
+
if (account === null)
|
|
90
|
+
return null;
|
|
91
|
+
io.persist(configDir, service);
|
|
92
|
+
return { service, account };
|
|
93
|
+
}
|
|
94
|
+
/** Real `security`/filesystem implementation of KeychainIO. Every method absorbs
|
|
95
|
+
* its own errors so a Keychain fault degrades to token-fallback rather than
|
|
96
|
+
* throwing into the spawn path. */
|
|
97
|
+
export function realKeychainIO() {
|
|
98
|
+
return {
|
|
99
|
+
readPersisted: (configDir) => {
|
|
100
|
+
try {
|
|
101
|
+
const s = readFileSync(join(configDir, PERSIST_FILENAME), 'utf8').trim();
|
|
102
|
+
return s.length > 0 ? s : null;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
persist: (configDir, service) => {
|
|
109
|
+
try {
|
|
110
|
+
writeFileSync(join(configDir, PERSIST_FILENAME), service, { mode: 0o600 });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Best-effort: a failed persist just means the next spawn re-discovers.
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
listItems: () => {
|
|
117
|
+
try {
|
|
118
|
+
return parseKeychainItems(execFileSync('security', ['dump-keychain'], { encoding: 'utf-8', timeout: 10_000 }));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
getAccount: (service) => {
|
|
125
|
+
try {
|
|
126
|
+
return parseKeychainAccount(execFileSync('security', ['find-generic-password', '-s', service], {
|
|
127
|
+
encoding: 'utf-8',
|
|
128
|
+
timeout: 10_000,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
writePassword: (service, account, blob) => {
|
|
136
|
+
try {
|
|
137
|
+
// The blob (incl. the refresh token) rides the argv of `security`, so it
|
|
138
|
+
// is briefly visible to same-user `ps -ww`. `add-generic-password` has no
|
|
139
|
+
// stdin path for the password, and the source file is already same-user-
|
|
140
|
+
// readable, so this adds no exposure beyond what a same-user attacker has.
|
|
141
|
+
execFileSync('security', ['add-generic-password', '-U', '-s', service, '-a', account, '-w', blob], { timeout: 10_000 });
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** The OS username, the `acct` the CLI stores its credentials item under. */
|
|
151
|
+
export function currentAccount() {
|
|
152
|
+
try {
|
|
153
|
+
return userInfo().username;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return '';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=claude-keychain-write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-keychain-write.js","sourceRoot":"","sources":["../src/claude-keychain-write.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type ConnectorVerdict = 'not-expected' | 'blocked-by-env-token' | 'pending' | 'expected-not-loaded' | 'ok';
|
|
2
|
+
/** True when the credentials file body carries a non-empty claudeAiOauth
|
|
3
|
+
* accessToken AND the `user:mcp_servers` scope (both gates for the CLI's
|
|
4
|
+
* account-connector fetch). Never throws. Pure. */
|
|
5
|
+
export declare function credsHaveConnectorScope(fileBody: string): boolean;
|
|
6
|
+
/** Count the connected `claude.ai …` servers in `claude mcp list` output. Only
|
|
7
|
+
* `✔ Connected` claude.ai entries count; `! Needs authentication` and non-
|
|
8
|
+
* claude.ai servers are excluded. Pure. */
|
|
9
|
+
export declare function parseLoadedClaudeAiConnectors(mcpListOutput: string): number;
|
|
10
|
+
export type SpawnAuthMode = 'keychain-writeback' | 'token-fallback' | 'none';
|
|
11
|
+
export interface ClassifyInput {
|
|
12
|
+
expected: boolean;
|
|
13
|
+
mode: SpawnAuthMode;
|
|
14
|
+
/** Number of loaded claude.ai connectors, or null when not yet probed. */
|
|
15
|
+
loaded: number | null;
|
|
16
|
+
}
|
|
17
|
+
/** Reduce expected/mode/loaded to a single verdict. When connectors are expected
|
|
18
|
+
* but the spawn fell back to env-token injection, the env token is the known
|
|
19
|
+
* suppressor — `blocked-by-env-token` — regardless of a later probe. Pure. */
|
|
20
|
+
export declare function classifyConnectorAvailability(input: ClassifyInput): ConnectorVerdict;
|
|
21
|
+
export interface AuditInput {
|
|
22
|
+
configDir: string;
|
|
23
|
+
mode: SpawnAuthMode;
|
|
24
|
+
readCredsFile: (configDir: string) => string;
|
|
25
|
+
runMcpList: (configDir: string) => string;
|
|
26
|
+
log: (line: string) => void;
|
|
27
|
+
}
|
|
28
|
+
export interface AuditResult {
|
|
29
|
+
expected: boolean;
|
|
30
|
+
loaded: number;
|
|
31
|
+
verdict: ConnectorVerdict;
|
|
32
|
+
}
|
|
33
|
+
/** Run the full reconciliation: read the file to decide `expected`, probe the CLI
|
|
34
|
+
* for `loaded`, classify, and emit one `op=connector-availability` line. */
|
|
35
|
+
export declare function auditConnectorAvailability(input: AuditInput): AuditResult;
|
|
36
|
+
//# sourceMappingURL=connector-availability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connector-availability.d.ts","sourceRoot":"","sources":["../src/connector-availability.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,sBAAsB,GACtB,SAAS,GACT,qBAAqB,GACrB,IAAI,CAAA;AAER;;oDAEoD;AACpD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAYjE;AAED;;4CAE4C;AAC5C,wBAAgB,6BAA6B,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAM3E;AAED,MAAM,MAAM,aAAa,GAAG,oBAAoB,GAAG,gBAAgB,GAAG,MAAM,CAAA;AAE5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,OAAO,CAAA;IACjB,IAAI,EAAE,aAAa,CAAA;IACnB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED;;+EAE+E;AAC/E,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,aAAa,GAAG,gBAAgB,CAKpF;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,aAAa,CAAA;IACnB,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAA;IAC5C,UAAU,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAA;IACzC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,OAAO,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,gBAAgB,CAAA;CAC1B;AAED;6EAC6E;AAC7E,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,UAAU,GAAG,WAAW,CAQzE"}
|