@humwick/runner 0.130.5 → 0.130.6
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/agent-bin/humwick +2 -2
- package/agent-bin/humwick-browser +4 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +1 -1
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +2 -2
- package/package.json +7 -10
- package/plugins/browser/skills/playwright-cli/SKILL.md +8 -6
- package/plugins/claude/.claude-plugin/plugin.json +3 -3
- package/plugins/claude/claude-statusline-probe.cjs +26 -16
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +47 -17
- package/plugins/claude/hooks/gate-run-guard.sh +3 -3
- package/plugins/claude/hooks/gate-run-guard.ts +5 -5
- package/plugins/claude/hooks/kill-guard.sh +4 -4
- package/plugins/claude/hooks/kill-guard.ts +1 -1
- package/plugins/claude/hooks/permission-request.sh +1 -1
- package/plugins/claude/hooks/post-compact.sh +1 -1
- package/plugins/claude/hooks/post-tool-use.sh +3 -3
- package/plugins/claude/hooks/pre-compact.sh +2 -2
- package/plugins/claude/hooks/read-only-bash-guard.sh +4 -4
- package/plugins/claude/hooks/relay-status.sh +32 -32
- package/plugins/claude/hooks/session-end.sh +2 -2
- package/plugins/claude/hooks/session-start.sh +3 -3
- package/plugins/claude/hooks/shell-trust.ts +1 -1
- package/plugins/claude/hooks/stop-failure.sh +2 -2
- package/plugins/claude/hooks/stop.sh +6 -6
- package/plugins/claude/hooks/subagent-skill-policy.cjs +8 -6
- package/plugins/claude/hooks/subagent-stop.sh +3 -3
- package/plugins/claude/hooks/user-prompt-submit.sh +1 -1
- package/plugins/claude/hooks/wait-loop-guard.ts +3 -3
- package/plugins/claude/monitors/monitors.json +2 -2
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +96 -62
- package/plugins/claude/monitors/relay-monitor.ts +5 -5
- package/plugins/claude/skills/label/SKILL.md +3 -3
- package/plugins/claude/skills/message/SKILL.md +5 -5
- package/plugins/claude/skills/react/SKILL.md +1 -1
- package/plugins/claude/skills/read-message/SKILL.md +4 -4
- package/plugins/claude/skills/reply/SKILL.md +5 -5
- package/plugins/claude/skills/send-claimable/SKILL.md +5 -5
- package/plugins/claude/skills/slice-first/SKILL.md +5 -3
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +30 -29
- package/plugins/claude/skills/status/SKILL.md +1 -1
- package/plugins/claude/skills/tags/SKILL.md +5 -5
- package/plugins/claude/skills/task/SKILL.md +10 -10
- package/plugins/claude-output-style/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-output-style/output-styles/{agent-relay.md → humwick.md} +1 -1
- package/plugins/codex/skills/guide/SKILL.md +1 -1
- package/plugins/codex/skills/label/SKILL.md +3 -3
- package/plugins/codex/skills/message/SKILL.md +5 -5
- package/plugins/codex/skills/react/SKILL.md +1 -1
- package/plugins/codex/skills/read-message/SKILL.md +4 -4
- package/plugins/codex/skills/reply/SKILL.md +5 -5
- package/plugins/codex/skills/send-claimable/SKILL.md +5 -5
- package/plugins/codex/skills/slice-first/SKILL.md +5 -3
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +30 -29
- package/plugins/codex/skills/status/SKILL.md +1 -1
- package/plugins/codex/skills/tags/SKILL.md +5 -5
- package/plugins/codex/skills/task/SKILL.md +11 -10
- package/src/adapter.ts +95 -92
- package/src/adapters/claude-api.ts +8 -11
- package/src/adapters/claude-context-command.ts +1 -1
- package/src/adapters/claude-credentials.ts +7 -3
- package/src/adapters/claude-delivery-lanes.ts +2 -2
- package/src/adapters/claude-delivery.ts +16 -16
- package/src/adapters/claude-effort.ts +2 -2
- package/src/adapters/claude-exit.ts +2 -2
- package/src/adapters/claude-headless-mirror.ts +18 -2
- package/src/adapters/claude-headless-refusal.ts +3 -3
- package/src/adapters/claude-headless.ts +11 -10
- package/src/adapters/claude-launch-exit.ts +1 -1
- package/src/adapters/claude-monitor-delivery-plan.ts +36 -16
- package/src/adapters/claude-permission.ts +5 -5
- package/src/adapters/claude-prompt-gates.ts +9 -9
- package/src/adapters/claude-prompt-hook.ts +2 -2
- package/src/adapters/claude-session-capture.ts +6 -6
- package/src/adapters/claude-session-probe.ts +25 -25
- package/src/adapters/claude-shutdown.ts +11 -11
- package/src/adapters/claude-status-detectors.ts +31 -31
- package/src/adapters/claude-subagents.ts +7 -7
- package/src/adapters/claude-submit-confirmation.ts +6 -6
- package/src/adapters/claude-tmux.ts +43 -43
- package/src/adapters/claude-token-usage-capture.ts +5 -5
- package/src/adapters/claude-tool-policy.ts +7 -7
- package/src/adapters/claude-transcript.ts +24 -24
- package/src/adapters/claude.ts +100 -100
- package/src/adapters/codex-agent-message-capture.ts +26 -63
- package/src/adapters/codex-app-server.ts +1 -1
- package/src/adapters/codex-client.ts +9 -9
- package/src/adapters/codex-effective-config.ts +1 -1
- package/src/adapters/codex-launch-config.ts +5 -5
- package/src/adapters/codex-rollout-context.ts +2 -2
- package/src/adapters/codex-skill-discovery.ts +1 -1
- package/src/adapters/codex-token-usage-capture.ts +16 -16
- package/src/adapters/codex-tool-output.ts +4 -4
- package/src/adapters/codex-version.ts +3 -3
- package/src/adapters/codex.ts +64 -64
- package/src/adapters/opencode-api.ts +1 -1
- package/src/adapters/opencode-context.ts +20 -22
- package/src/adapters/opencode-model-registry.ts +7 -7
- package/src/adapters/opencode-provider.ts +8 -8
- package/src/adapters/opencode-provisioning.ts +30 -0
- package/src/adapters/opencode-server-retry.ts +1 -1
- package/src/adapters/opencode-soft-steer.ts +5 -5
- package/src/adapters/opencode-spawn.ts +18 -14
- package/src/adapters/opencode-token-usage-capture.ts +12 -12
- package/src/adapters/opencode-tool-events.ts +15 -15
- package/src/adapters/opencode.ts +232 -232
- package/src/adapters/project-shell.ts +11 -11
- package/src/adapters/service-access.ts +6 -6
- package/src/adapters/tmux-offload.ts +9 -9
- package/src/adapters/tool-summary.ts +5 -5
- package/src/agent-redact.ts +1 -1
- package/src/agent-runner/commands/dispatch.ts +10 -10
- package/src/agent-runner/commands/lifecycle.ts +2 -2
- package/src/agent-runner/commands/prompt.ts +19 -19
- package/src/agent-runner/credentials.ts +5 -5
- package/src/agent-runner/inbox.ts +1 -1
- package/src/agent-runner/lifecycle-milestones.ts +3 -3
- package/src/agent-runner/lifecycle-phase.ts +2 -2
- package/src/agent-runner/liveness-exchange.ts +4 -10
- package/src/agent-runner/registration.ts +8 -8
- package/src/agent-runner/runner-context.ts +9 -14
- package/src/agent-runner/session-mirror.ts +12 -12
- package/src/agent-runner/task-claims.ts +2 -2
- package/src/agent-runner/token-usage-coverage.ts +1 -1
- package/src/agent-runner/turn-report.ts +12 -12
- package/src/api-budget-accounting.ts +3 -3
- package/src/area-memory.ts +6 -6
- package/src/artifact-path-materialize.ts +1 -1
- package/src/async-guard.ts +5 -5
- package/src/attachment-cache.ts +17 -22
- package/src/background-script-reconcile.ts +6 -6
- package/src/bounded-log.ts +1 -1
- package/src/busy-reconciler.ts +22 -22
- package/src/claim-tracker.ts +3 -3
- package/src/claude-settings.ts +8 -8
- package/src/claude-system-prompt.ts +1 -1
- package/src/claude-workspace-trust.ts +1 -1
- package/src/config.ts +62 -61
- package/src/context-boundary.ts +5 -5
- package/src/context-command.ts +3 -3
- package/src/control-server.ts +32 -32
- package/src/delivery-result.ts +2 -2
- package/src/effort-command.ts +2 -2
- package/src/empty-turn.ts +22 -14
- package/src/free-port.ts +1 -1
- package/src/gate-run-config.ts +3 -3
- package/src/host-asset-plan.ts +24 -17
- package/src/host-claude-config-worker.ts +2 -2
- package/src/host-sources.ts +2 -2
- package/src/index.ts +16 -16
- package/src/launch-assembly.ts +94 -96
- package/src/liveness.ts +15 -15
- package/src/logger.ts +3 -3
- package/src/mcp-outbox.ts +3 -3
- package/src/mcp-preflight.ts +2 -2
- package/src/mcp-source-selection.ts +2 -2
- package/src/mcp-tool-deferral.ts +1 -1
- package/src/mcp-vault-env.ts +2 -2
- package/src/message-body-config/config.ts +4 -4
- package/src/mid-turn-delivery.ts +2 -2
- package/src/native-memory-lever.ts +8 -8
- package/src/outbox.ts +15 -15
- package/src/pending-permission-store.ts +1 -1
- package/src/profile-home.ts +19 -19
- package/src/profile-projection.ts +26 -24
- package/src/project-instructions.ts +5 -5
- package/src/provider-auth-warning.ts +17 -0
- package/src/provider-double.ts +1 -1
- package/src/provider-hold.ts +3 -3
- package/src/provider-home-marker.ts +1 -1
- package/src/provider-home-tombstone.ts +1 -1
- package/src/provider-input-queue.ts +2 -2
- package/src/provider-integration.ts +15 -13
- package/src/provider-setup.ts +15 -3
- package/src/providers.ts +1 -1
- package/src/provisioning.ts +37 -40
- package/src/question-decision.ts +2 -2
- package/src/quota.ts +0 -6
- package/src/rate-limit.ts +24 -13
- package/src/refused-prompt-store.ts +2 -2
- package/src/relay-injection-events.ts +2 -2
- package/src/relay-instructions.ts +5 -5
- package/src/relay-mcp-proxy.long-turn-e2e.ts +13 -13
- package/src/relay-mcp-proxy.ts +57 -58
- package/src/relay-mcp.ts +55 -69
- package/src/reply-obligation-cache.ts +9 -9
- package/src/response-capture-report.ts +9 -9
- package/src/response-routing.ts +1 -1
- package/src/runner-core.ts +209 -222
- package/src/runner-exit.ts +1 -1
- package/src/runner-helpers.ts +10 -10
- package/src/runner-insights.ts +4 -2
- package/src/runner-mcp-proxy.ts +3 -3
- package/src/runner-spawn-config.ts +3 -3
- package/src/runtime-token-file.ts +1 -1
- package/src/runtime-token-remint.ts +1 -1
- package/src/service-access.ts +2 -2
- package/src/session-destroy.ts +1 -1
- package/src/session-insights.ts +1 -1
- package/src/session-scratch.ts +24 -37
- package/src/spawn-input.ts +6 -6
- package/src/startup-config-fetch.ts +4 -4
- package/src/subreaper.ts +3 -5
- package/src/test-run-dedup.ts +13 -13
- package/src/transcript-context.ts +2 -2
- package/src/transport-errors.ts +1 -1
- package/src/turn-opener.ts +1 -1
- package/src/turn-reconcile-event.ts +2 -2
- package/src/vault-proxy-hop.ts +2 -2
- package/src/web-tools.ts +2 -2
- package/agent-bin/agent-relay-browser +0 -10
- package/src/adapters/provider-quota-harvest.ts +0 -76
package/src/liveness.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { PROVIDER_TURN_EMPTY_TIMELINE_STATUS, PROVIDER_TURN_WATCHDOG_TIMELINE_STATUS, RECONCILED_TURN_TIMELINE_STATUS } from "@humwick/sdk";
|
|
2
2
|
import type { BusyPhase, LivenessSignal } from "@humwick/sdk";
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
// held-but-unproven turn), an end (a force-cleared one), or —
|
|
4
|
+
// these timeline statuses are the runner ADMITTING it could not confirm progress (a
|
|
5
|
+
// held-but-unproven turn), an end (a force-cleared one), or — a turn that demonstrably
|
|
6
6
|
// produced NOTHING. Folding their `timestamp` in as
|
|
7
7
|
// progress would be circular: the watchdog fires exactly when the runner has NO positive
|
|
8
8
|
// evidence, yet its own publish (`timestamp: now`) would otherwise stamp that same status frame
|
|
9
9
|
// as freshly-progressing, poisoning any consumer (e.g. the server's prolonged-hold coordinator
|
|
10
|
-
// wake
|
|
10
|
+
// wake) that reads `lastProgressAt` off it. Every OTHER timeline transition (e.g. Claude's
|
|
11
11
|
// `compacting`) is genuine forward progress and must keep counting.
|
|
12
|
-
//
|
|
12
|
+
// belongs here most sharply of the three: an empty-turn event is the runner reporting the
|
|
13
13
|
// ABSENCE of output, so stamping its own publish as fresh progress would let the frame that says
|
|
14
14
|
// "this agent did nothing" be read as evidence that it just did something.
|
|
15
15
|
const NON_PROGRESS_TIMELINE_STATUSES: ReadonlySet<string> = new Set([
|
|
@@ -19,7 +19,7 @@ const NON_PROGRESS_TIMELINE_STATUSES: ReadonlySet<string> = new Set([
|
|
|
19
19
|
]);
|
|
20
20
|
|
|
21
21
|
// The raw runner-tracked signals an adapter folds into a normalized LivenessSignal.
|
|
22
|
-
// This is the INPUT to the contract computation
|
|
22
|
+
// This is the INPUT to the contract computation: the runner owns this state
|
|
23
23
|
// (transport frames, the last provider event, the pending timeline transition, the
|
|
24
24
|
// active-work set), passes it to the adapter's `liveness()` verdict, and ships the
|
|
25
25
|
// resulting LivenessSignal on the agent card. Provider-raw interpretation stops here —
|
|
@@ -36,12 +36,12 @@ export interface LivenessInputs {
|
|
|
36
36
|
activeWork: Array<{ kind?: string; startedAt?: number }>;
|
|
37
37
|
/** Fallback progress floor when no other progress evidence exists yet (e.g. work start). */
|
|
38
38
|
fallbackProgressAt?: number;
|
|
39
|
-
/**
|
|
39
|
+
/** the runner's coarse provider status (`claims.currentStatus`), used together with
|
|
40
40
|
* `providerBlocked` + `activeWork` to derive {@link LivenessSignal.busyPhase}. Provider-agnostic:
|
|
41
41
|
* `busy` + a live provider-turn ⇒ `active` (the provider is doing turn work — including a
|
|
42
42
|
* token-silent foreground `shell` gate), regardless of which provider. Omitted → no phase. */
|
|
43
43
|
providerStatus?: "idle" | "busy" | "offline" | "error";
|
|
44
|
-
/**
|
|
44
|
+
/** the runner's provider-hold flag (rate-limit / pendingQuestion / a `blocked`
|
|
45
45
|
* providerState, e.g. Claude `waiting`). When set, the phase is `blocked` — the provider is
|
|
46
46
|
* parked, not actively computing — and takes precedence over `active`. */
|
|
47
47
|
providerBlocked?: boolean;
|
|
@@ -52,7 +52,7 @@ function num(value: unknown): number {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* The shared liveness fold
|
|
55
|
+
* The shared liveness fold. Both reference adapters
|
|
56
56
|
* route through this so the verdict is computed in ONE place; each adapter's `liveness()`
|
|
57
57
|
* decides WHICH raw signals it feeds in (Claude surfaces a `compacting` timeline transition
|
|
58
58
|
* as progress; Codex surfaces live subprocess work-kinds). The fold itself is provider-
|
|
@@ -63,8 +63,8 @@ export function computeLivenessSignal(inputs: LivenessInputs): LivenessSignal {
|
|
|
63
63
|
num(inputs.transport?.lastServerFrameAt),
|
|
64
64
|
num(inputs.transport?.lastHeartbeatAckAt),
|
|
65
65
|
);
|
|
66
|
-
// Progress = provider tokens OR a lifecycle/timeline transition (e.g. native compaction
|
|
67
|
-
//
|
|
66
|
+
// Progress = provider tokens OR a lifecycle/timeline transition (e.g. native compaction)
|
|
67
|
+
// — whichever is latest, with the caller-supplied floor as the baseline. A
|
|
68
68
|
// timeline event that is itself an admission of no-confirmed-progress (the prolonged-hold
|
|
69
69
|
// watchdog, or a reconciled/force-cleared turn) must NOT count as progress — see
|
|
70
70
|
// NON_PROGRESS_TIMELINE_STATUSES above.
|
|
@@ -77,7 +77,7 @@ export function computeLivenessSignal(inputs: LivenessInputs): LivenessSignal {
|
|
|
77
77
|
timelineProgressAt,
|
|
78
78
|
num(inputs.fallbackProgressAt),
|
|
79
79
|
);
|
|
80
|
-
// Any work-kind that is NOT the bare provider turn is live tool/subprocess work
|
|
80
|
+
// Any work-kind that is NOT the bare provider turn is live tool/subprocess work:
|
|
81
81
|
// a wedged provider turn with a live `bun test` child must read as progressing.
|
|
82
82
|
const hasLiveWork = inputs.activeWork.some((work) => Boolean(work.kind && work.kind !== "provider-turn"));
|
|
83
83
|
const busyPhase = deriveBusyPhase(inputs, hasLiveWork);
|
|
@@ -85,15 +85,15 @@ export function computeLivenessSignal(inputs: LivenessInputs): LivenessSignal {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
*
|
|
89
|
-
* agnostic by construction (the point of the
|
|
88
|
+
* derive the coarse {@link BusyPhase} from the runner's own state. Provider-
|
|
89
|
+
* agnostic by construction (the point of the contract): a live `provider-turn` while the
|
|
90
90
|
* runner status is `busy` is `active` for EVERY provider — for Claude that already covers both
|
|
91
91
|
* `busy` (thinking) and `shell` (a foreground gate), and for Codex it covers `inProgress`,
|
|
92
92
|
* because the adapter mints/holds the `provider-turn` work-kind for exactly those states. A
|
|
93
93
|
* provider hold (`providerBlocked`) is `blocked` and wins over `active` (a parked turn is not
|
|
94
|
-
* computing). Otherwise, live background-script work with no active turn is `passive
|
|
94
|
+
* computing). Otherwise, live background-script work with no active turn is `passive`.
|
|
95
95
|
* `hasLiveWork` stays the narrower "is there background-script work" projection so its existing
|
|
96
|
-
* stuck-busy veto (
|
|
96
|
+
* stuck-busy veto (a live `bun test` child during an active turn) is unchanged; `passive`
|
|
97
97
|
* always implies `hasLiveWork`, but `active` deliberately does not.
|
|
98
98
|
*/
|
|
99
99
|
function deriveBusyPhase(inputs: LivenessInputs, hasLiveWork: boolean): BusyPhase | undefined {
|
package/src/logger.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { sanitizeFsName } from "@humwick/sdk/fs-name";
|
|
|
4
4
|
import { logDirFromEnv, logLevelFromEnv } from "./config";
|
|
5
5
|
import { appendBoundedLog, RUNNER_LOG_MAX_BYTES, RUNNER_LOG_RECORD_MAX_BYTES } from "./bounded-log";
|
|
6
6
|
|
|
7
|
-
// Phase 1 observability
|
|
7
|
+
// Phase 1 observability: one leveled, runtime-togglable logger for the
|
|
8
8
|
// Runner and the provider adapters below it. Replaces the ad-hoc scatter of
|
|
9
9
|
// `console.error`, `logRunnerDiagnostic` (-> runner-<agent>.log) and
|
|
10
10
|
// `sessionLog`/`sessionDebug` (-> session-mirror-<agent>.log) with a single
|
|
@@ -14,7 +14,7 @@ import { appendBoundedLog, RUNNER_LOG_MAX_BYTES, RUNNER_LOG_RECORD_MAX_BYTES } f
|
|
|
14
14
|
// already surfaces to the dashboard log-viewer (captureSessionMirror). One place
|
|
15
15
|
// to look when anything in the Runner misbehaves.
|
|
16
16
|
//
|
|
17
|
-
// Level is read once from
|
|
17
|
+
// Level is read once from HUMWICK_LOG_LEVEL (default "info") and can be
|
|
18
18
|
// flipped at runtime via the control port (no restart) — so a phase refactor can
|
|
19
19
|
// be watched at debug without bouncing the agent.
|
|
20
20
|
|
|
@@ -55,7 +55,7 @@ export class Logger {
|
|
|
55
55
|
this.level = config.level ?? parseLogLevel(logLevelFromEnv()) ?? "info";
|
|
56
56
|
this.agentId = config.agentId ?? "runner";
|
|
57
57
|
this.headless = config.headless ?? false;
|
|
58
|
-
this.logDir = config.logDir ?? logDirFromEnv() ?? join(process.env.HOME || ".", ".
|
|
58
|
+
this.logDir = config.logDir ?? logDirFromEnv() ?? join(process.env.HOME || ".", ".humwick", "logs");
|
|
59
59
|
this.logFileMaxBytes = config.logFileMaxBytes ?? RUNNER_LOG_MAX_BYTES;
|
|
60
60
|
}
|
|
61
61
|
|
package/src/mcp-outbox.ts
CHANGED
|
@@ -108,7 +108,7 @@ async function deliverSessionBatchItems(input: RunnerOutboxDelivery, messages: S
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// Deterministic session validation rejections are classified once in runner-helpers.ts
|
|
111
|
-
// (isDeterministicClientError) and shared with the outbox poison policy
|
|
111
|
+
// (isDeterministicClientError) and shared with the outbox poison policy.
|
|
112
112
|
|
|
113
113
|
function sessionBatchItemKeys(record: OutboxRecord): string[] {
|
|
114
114
|
const messages = sessionBatchMessages(record);
|
|
@@ -122,7 +122,7 @@ function sessionBatchMessages(record: OutboxRecord): SendMessageInput[] {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
// The subset of RunnerOutboxDelivery the notice seam needs — also satisfied by the narrower
|
|
125
|
-
// Outbox.onPoison call site
|
|
125
|
+
// Outbox.onPoison call site, which only holds the poisoned record + the HTTP client.
|
|
126
126
|
type MirrorDropNoticeSource = Pick<RunnerOutboxDelivery, "record"> & { http: HumwickHttpClient };
|
|
127
127
|
|
|
128
128
|
async function emitMirrorDropNotice(input: MirrorDropNoticeSource, messages: SendMessageInput[], count: number, reason: string, suffix: string): Promise<void> {
|
|
@@ -158,7 +158,7 @@ function shortHash(value: string): string {
|
|
|
158
158
|
return (h >>> 0).toString(36);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
//
|
|
161
|
+
// the Outbox.onPoison seam for SESSION rows. A permanently-dropped session-message /
|
|
162
162
|
// batch is REAL report-up/mirror loss (chat replyTo lineage rides the session outbox), so
|
|
163
163
|
// surface it in-session through the same notice channel as duplicate/invalid drops instead
|
|
164
164
|
// of only in a FATAL host log. Non-session kinds carry no chat lineage to notify about.
|
package/src/mcp-preflight.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { hostname } from "node:os";
|
|
|
3
3
|
import { basename, isAbsolute } from "node:path";
|
|
4
4
|
import { PORTABLE_SHARED_MCP_SERVER_NAMES, activeSharedMcpServers, portableMcpDescriptorPath, type PortableMcpDescriptor, type ProvisioningMcpServer } from "@humwick/sdk";
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// a registry descriptor is authored on one host (e.g. the hub's `~/.npm-global/bin/<server>`)
|
|
7
7
|
// but launched on any. A stdio command that does not exist on THIS host would reach the agent as a
|
|
8
8
|
// broken server ("MCP server failed to start (ENOENT)"). A core shared server (tokenlean/...) is
|
|
9
9
|
// swapped for the descriptor this host's orchestrator provisioned (same binary + URL the shared
|
|
@@ -35,7 +35,7 @@ export interface McpPreflightOptions {
|
|
|
35
35
|
sharedListener: boolean;
|
|
36
36
|
/** Names exempt from the check (their command is provisioned after launch resolution). */
|
|
37
37
|
skip?: readonly string[];
|
|
38
|
-
/**
|
|
38
|
+
/** HUMWICK_HOME of this host, where the orchestrator records provisioned core descriptors. */
|
|
39
39
|
home?: string;
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -22,7 +22,7 @@ export interface McpSourceSelection {
|
|
|
22
22
|
sourceShadows: ResolvedSourceShadow[];
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// providers load their native host declarations, then share this precedence policy.
|
|
26
26
|
// Serialization, service-access overlays and tool deferral consume the selected sources.
|
|
27
27
|
export function resolveMcpSources(
|
|
28
28
|
config: RunnerSpawnConfig,
|
|
@@ -39,7 +39,7 @@ export function resolveMcpSources(
|
|
|
39
39
|
);
|
|
40
40
|
const projectServers = Object.keys(projectMcp);
|
|
41
41
|
|
|
42
|
-
//
|
|
42
|
+
// sharedMcp is a resolved sibling opt-in, independent of ambient H.mcp.
|
|
43
43
|
const sharedListenerUrl = config.agentProfile?.sharedMcp ? resolveSharedMcpUrl(config.sharedMcpUrl) : undefined;
|
|
44
44
|
const sharedMcp: Record<string, ProvisioningMcpServer> = sharedListenerUrl
|
|
45
45
|
? { [SHARED_MCP_SERVER_NAME]: sharedMcpBridgeServer(sharedListenerUrl, config.cwd) }
|
package/src/mcp-tool-deferral.ts
CHANGED
|
@@ -28,7 +28,7 @@ export function runnerManagedRelayOnDemandTools(
|
|
|
28
28
|
|| !isCodexLeanWorkerProfile(profile?.name)
|
|
29
29
|
|| !profile?.sharedMcp
|
|
30
30
|
|| profile.relay?.mcp === false
|
|
31
|
-
//
|
|
31
|
+
// a switched-off web tool keeps the flat bridge (launch-assembly), so nothing fronts this set.
|
|
32
32
|
|| WEB_TOOL_NAMES.some((name) => !profileWebToolEnabled(profile, name))
|
|
33
33
|
) return undefined;
|
|
34
34
|
return new Set(CODEX_WORKER_ONDEMAND_TOOLS);
|
package/src/mcp-vault-env.ts
CHANGED
|
@@ -4,11 +4,11 @@ import { shellQuote } from "@humwick/sdk/shell-utils";
|
|
|
4
4
|
|
|
5
5
|
/** Codex has env_vars inheritance but no Claude-style interpolation in stdio env. */
|
|
6
6
|
export function codexVaultMcpEnv(server: ProvisioningMcpServer): ProvisioningMcpServer {
|
|
7
|
-
const env = writeChildEnv({}, { ...server.env });
|
|
7
|
+
const env = writeChildEnv({}, { ...server.env }, []);
|
|
8
8
|
const assignments: string[] = [];
|
|
9
9
|
const slots = new Set<string>();
|
|
10
10
|
for (const [key, value] of Object.entries(env)) {
|
|
11
|
-
const match = /^\$\{(
|
|
11
|
+
const match = /^\$\{(HUMWICK_VAULT_[A-F0-9]{24})\}$/.exec(value);
|
|
12
12
|
if (!match) continue;
|
|
13
13
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || !server.command) throw new Error("Invalid vault MCP environment projection");
|
|
14
14
|
assignments.push(`export ${key}="\${${match[1]}:?}"`);
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { relayEnv } from "@humwick/sdk/env";
|
|
2
2
|
export function messageBodyMaxCharsFromEnv(): string | undefined {
|
|
3
|
-
return relayEnv(process.env, "
|
|
3
|
+
return relayEnv(process.env, "HUMWICK_MESSAGE_BODY_MAX_CHARS");
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// the Claude Code monitor delivery caps (empirically measured against the live harness on
|
|
7
7
|
// 2026-08-02: ~3000 chars per delivered event, ~500 chars per line). Overridable per deployment,
|
|
8
8
|
// since the right ceiling tracks the host's harness build — set on the host/orchestrator that
|
|
9
9
|
// spawns runners. See DEFAULT_MONITOR_DELIVERY_* in adapter.ts for the resolved defaults.
|
|
10
10
|
export function monitorDeliveryMaxCharsFromEnv(): string | undefined {
|
|
11
|
-
return relayEnv(process.env, "
|
|
11
|
+
return relayEnv(process.env, "HUMWICK_MONITOR_DELIVERY_MAX_CHARS");
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function monitorDeliveryLineMaxCharsFromEnv(): string | undefined {
|
|
15
|
-
return relayEnv(process.env, "
|
|
15
|
+
return relayEnv(process.env, "HUMWICK_MONITOR_DELIVERY_LINE_MAX_CHARS");
|
|
16
16
|
}
|
package/src/mid-turn-delivery.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Phase A — the shared soft-steer foundation.
|
|
2
2
|
//
|
|
3
3
|
// One adapter-owned eligibility decision (`partitionMidTurnDelivery`), one config seam
|
|
4
4
|
// (`resolveActiveTurnMessageDelivery`) and one acceptance-correlation id (`midTurnClientUserId`),
|
|
@@ -32,7 +32,7 @@ function isSteerableMidTurnMessage(message: Message): boolean {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Stable acceptance-correlation id for one steered composition
|
|
35
|
+
* Stable acceptance-correlation id for one steered composition. Derived from
|
|
36
36
|
* the ordered batch ids, so a re-offer of the SAME subset reuses the id: the app-server echoes
|
|
37
37
|
* it back as the `userMessage.clientId` item field, and an echo observed after a lost steer
|
|
38
38
|
* response lets the next offer settle by correlation instead of re-steering (which would
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeChildEnv } from "@humwick/sdk/env";
|
|
2
|
-
//
|
|
2
|
+
// P5.7 (design §10.3) — the provider-NATIVE-memory lever.
|
|
3
3
|
//
|
|
4
4
|
// WHAT THIS IS FOR. Every agent today can carry two competing memory stores: relay memory (which
|
|
5
5
|
// survives a respawn) and the provider's own (which, for 8 of 11 built-in profiles, is destroyed
|
|
@@ -27,7 +27,7 @@ import { writeChildEnv } from "@humwick/sdk/env";
|
|
|
27
27
|
//
|
|
28
28
|
// NEVER THROWS. Resolution is total: any unexpected input degrades to "nothing applied, reported
|
|
29
29
|
// as suppressed" rather than propagating. A failure to apply this lever must not fail a spawn
|
|
30
|
-
// (the
|
|
30
|
+
// (the discipline, applied to the switch instead of the preamble) — an agent that launches
|
|
31
31
|
// with its native memory still on is a two-store agent, which is exactly today's state; an agent
|
|
32
32
|
// that fails to launch is an outage.
|
|
33
33
|
|
|
@@ -63,7 +63,7 @@ export interface NativeMemoryLever {
|
|
|
63
63
|
/** The levers that actually reach this launch, as short stable ids for the projection report. */
|
|
64
64
|
applied: string[];
|
|
65
65
|
/** Levers that were resolved but could NOT be delivered on this base/provider. Reported LOUDLY
|
|
66
|
-
*
|
|
66
|
+
* so a lever is never silently absent while the
|
|
67
67
|
* report claims it applied. */
|
|
68
68
|
suppressed?: { reason: string; levers: string[] };
|
|
69
69
|
}
|
|
@@ -107,7 +107,7 @@ function resolveClaudeLever(
|
|
|
107
107
|
// The env var is unconditional and authoritative — it works on EVERY base, host included, and
|
|
108
108
|
// outranks the settings read inside the CLI's own predicate.
|
|
109
109
|
const disable = policy === "off";
|
|
110
|
-
const env = writeChildEnv({}, { [CLAUDE_DISABLE_AUTO_MEMORY_ENV]: disable ? "1" : "0" });
|
|
110
|
+
const env = writeChildEnv({}, { [CLAUDE_DISABLE_AUTO_MEMORY_ENV]: disable ? "1" : "0" }, []);
|
|
111
111
|
const applied = [`env:${CLAUDE_DISABLE_AUTO_MEMORY_ENV}=${disable ? "1" : "0"}`];
|
|
112
112
|
|
|
113
113
|
const settings: Record<string, unknown> = { [CLAUDE_AUTO_MEMORY_ENABLED_SETTING]: !disable };
|
|
@@ -179,7 +179,7 @@ type NativeMemoryLeverRenderer = (
|
|
|
179
179
|
input: ResolveNativeMemoryLeverInput,
|
|
180
180
|
) => NativeMemoryLever;
|
|
181
181
|
|
|
182
|
-
//
|
|
182
|
+
// the claude renderer is shared by every claude-family provider id (claude-b reuses
|
|
183
183
|
// the claude adapter/launch path whole), so the absent⇒off invariant holds for it too.
|
|
184
184
|
const claudeLeverRenderer: NativeMemoryLeverRenderer = (policy, input) => resolveClaudeLever(
|
|
185
185
|
policy,
|
|
@@ -238,7 +238,7 @@ export function resolveNativeMemoryLever(input: ResolveNativeMemoryLeverInput):
|
|
|
238
238
|
suppressed: { reason: `provider ${input.provider} has no known native-memory lever; nothing was applied`, levers: ["nativeMemory"] },
|
|
239
239
|
};
|
|
240
240
|
} catch (err) {
|
|
241
|
-
// The fail-safe (
|
|
241
|
+
// The fail-safe (discipline applied to the switch). A lever we could not compute must
|
|
242
242
|
// degrade to "not applied, loudly reported" — never to a failed spawn.
|
|
243
243
|
return {
|
|
244
244
|
...none(),
|
|
@@ -293,7 +293,7 @@ export function claudeManagedNativeMemorySettings(
|
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
/**
|
|
296
|
-
* The fail-safe at the APPLY seam (
|
|
296
|
+
* The fail-safe at the APPLY seam (discipline, moved from the preamble to the switch). The
|
|
297
297
|
* file-channel belt is best-effort by construction: the authoritative lever is the env var / `-c`
|
|
298
298
|
* arg, computed into the launch and touching no disk, so a failed belt write costs nothing an
|
|
299
299
|
* operator can observe — while a THROW here would fail the whole spawn. So: never propagate, and
|
|
@@ -305,7 +305,7 @@ export function applyNativeMemoryBelt(label: string, apply: () => void): string
|
|
|
305
305
|
return undefined;
|
|
306
306
|
} catch (err) {
|
|
307
307
|
const reason = `native-memory lever: ${label} could not be written: ${(err as Error)?.message ?? String(err)}. `
|
|
308
|
-
+ "The authoritative env/argv lever still applies (it outranks the file channel), and the spawn continues — design
|
|
308
|
+
+ "The authoritative env/argv lever still applies (it outranks the file channel), and the spawn continues — design §10.3.";
|
|
309
309
|
console.warn(reason);
|
|
310
310
|
return reason;
|
|
311
311
|
}
|
package/src/outbox.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { logger } from "./logger";
|
|
|
8
8
|
import { runnerOutboxDirFromEnv } from "./config";
|
|
9
9
|
import { isDeterministicClientError, isTransientDeliveryError } from "./runner-helpers";
|
|
10
10
|
|
|
11
|
-
// Phase 2
|
|
11
|
+
// Phase 2 — the "nothing is ever lost" half. Runner→server events that used to be
|
|
12
12
|
// fire-and-forget over HTTP (session turns, reasoning/tool traces, prompt echoes, insights,
|
|
13
13
|
// hook-fatal reports) were silently dropped whenever the server was momentarily down. This
|
|
14
14
|
// is a durable, FIFO, disk-backed queue that:
|
|
@@ -18,7 +18,7 @@ import { isDeterministicClientError, isTransientDeliveryError } from "./runner-h
|
|
|
18
18
|
// reorder turns),
|
|
19
19
|
// - poisons a DETERMINISTICALLY-failing head immediately and an unknown-failure head after
|
|
20
20
|
// maxAttempts so it can't block the queue — but NEVER poisons on attempt count for
|
|
21
|
-
// TRANSIENT (connection-class / 5xx / 429) failures
|
|
21
|
+
// TRANSIENT (connection-class / 5xx / 429) failures: report-up chat rides this
|
|
22
22
|
// queue, so a poisoned row is real message loss. A transient head instead parks onto a
|
|
23
23
|
// slow periodic re-drive after a long CONTINUOUS-failure wall-clock budget, so an ordinary
|
|
24
24
|
// relay-restart window can't drop anything no matter how hard flush() hammers,
|
|
@@ -58,14 +58,14 @@ type OutboxSend = (record: OutboxRecord) => Promise<void>;
|
|
|
58
58
|
interface OutboxOptions {
|
|
59
59
|
agentId: string;
|
|
60
60
|
send: OutboxSend;
|
|
61
|
-
// Storage directory. Defaults to
|
|
61
|
+
// Storage directory. Defaults to HUMWICK_RUNNER_OUTBOX_DIR, else a per-host temp dir.
|
|
62
62
|
dir?: string;
|
|
63
63
|
maxRows?: number;
|
|
64
64
|
maxAttempts?: number;
|
|
65
65
|
baseBackoffMs?: number;
|
|
66
66
|
maxBackoffMs?: number;
|
|
67
67
|
pollMs?: number;
|
|
68
|
-
//
|
|
68
|
+
// transient (connection-class / 5xx / 429) failures NEVER poison on attempt count.
|
|
69
69
|
// After this much CONTINUOUS wall-clock failure on a row, park it onto a slow periodic
|
|
70
70
|
// re-drive (every transientRedriveMs) instead of hot-looping, so a long relay outage still
|
|
71
71
|
// recovers every frame once the relay returns. Sized to outlast any ordinary restart/deploy
|
|
@@ -75,7 +75,7 @@ interface OutboxOptions {
|
|
|
75
75
|
// Observability seam: called when a row is actually POISONED (permanently dropped). Only
|
|
76
76
|
// deterministic client rejections and unknown-class failures past maxAttempts get here —
|
|
77
77
|
// never transient ones. The session outbox uses it to surface mirror/report-up loss
|
|
78
|
-
// in-session
|
|
78
|
+
// in-session rather than only in a FATAL host log.
|
|
79
79
|
onPoison?: (record: OutboxRecord, reason: string) => void | Promise<void>;
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -142,7 +142,7 @@ export class Outbox {
|
|
|
142
142
|
this.transientRedriveMs = options.transientRedriveMs ?? DEFAULTS.transientRedriveMs;
|
|
143
143
|
this.onPoison = options.onPoison;
|
|
144
144
|
|
|
145
|
-
const dir = options.dir ?? runnerOutboxDirFromEnv() ?? join(tmpdir(), "
|
|
145
|
+
const dir = options.dir ?? runnerOutboxDirFromEnv() ?? join(tmpdir(), "humwick-outbox");
|
|
146
146
|
this.path = options.dir === ":memory:" ? ":memory:" : join(dir, `outbox-${safeName(this.agentId)}.sqlite`);
|
|
147
147
|
if (this.path !== ":memory:") mkdirSync(dirname(this.path), { recursive: true });
|
|
148
148
|
|
|
@@ -165,7 +165,7 @@ export class Outbox {
|
|
|
165
165
|
created_at INTEGER NOT NULL
|
|
166
166
|
)
|
|
167
167
|
`);
|
|
168
|
-
//
|
|
168
|
+
// Previous databases lack the transient-park bookkeeping column (fresh CREATEs include it).
|
|
169
169
|
const columns = this.db.query("PRAGMA table_info(outbox)").all() as Array<{ name: string }>;
|
|
170
170
|
if (!columns.some((column) => column.name === "first_failed_at")) {
|
|
171
171
|
this.db.exec("ALTER TABLE outbox ADD COLUMN first_failed_at INTEGER NOT NULL DEFAULT 0");
|
|
@@ -229,13 +229,13 @@ export class Outbox {
|
|
|
229
229
|
this.pollTimer.unref?.();
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
//
|
|
232
|
+
// the ONE way a background caller starts a drain. `drain` is a bare try/finally with
|
|
233
233
|
// no catch, and drainOnce() touches SQLite outside the send() try/catch (the head SELECT, and the
|
|
234
234
|
// attempt/poison UPDATEs in the failure path), so a closed or erroring database rejects it. Every
|
|
235
235
|
// pump site is fire-and-forget — an enqueue microtask, start()'s initial pump and poll timer, the
|
|
236
236
|
// scheduleDue wake-up — with nobody to await the promise, and Bun exits(1) on an unhandled
|
|
237
|
-
// rejection with no handler installed anywhere in the runner
|
|
238
|
-
// agent over a queue error, on a runner nothing respawns
|
|
237
|
+
// rejection with no handler installed anywhere in the runner. That crashed the whole
|
|
238
|
+
// agent over a queue error, on a runner nothing respawns.
|
|
239
239
|
//
|
|
240
240
|
// Reported through this module's own existing channel (logger, same as the bound-exceeded warn
|
|
241
241
|
// and the poison fatal) rather than swallowed: a pump that stops pumping is its own bug, and
|
|
@@ -248,13 +248,13 @@ export class Outbox {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
// Drain everything we can before the process exits, ignoring per-row backoff. Used on
|
|
251
|
-
// shutdown/kill/crash
|
|
251
|
+
// shutdown/kill/crash: the capture seam durably enqueues the end-of-session
|
|
252
252
|
// Insights datapoint, but the per-agent outbox is never reopened once the agent is gone,
|
|
253
253
|
// so a row that hasn't been POSTed yet is stranded forever. flush() blocks (bounded) on
|
|
254
254
|
// delivery — the opposite intent of the polite, backoff-respecting steady-state drain.
|
|
255
255
|
// Returns true if the queue fully drained within the deadline.
|
|
256
256
|
//
|
|
257
|
-
//
|
|
257
|
+
// the forced 150ms retry loop here used to consume the per-row attempt/poison
|
|
258
258
|
// budget, so <2s of relay downtime at a flush boundary poisoned frames. Force-drain
|
|
259
259
|
// failures no longer burn the TRANSIENT poison budget (see drainOnce) — flush() can
|
|
260
260
|
// hammer as hard as it likes during an outage without ever pushing a row toward poison.
|
|
@@ -314,7 +314,7 @@ export class Outbox {
|
|
|
314
314
|
const reason = errMessage(error);
|
|
315
315
|
const transient = isTransientDeliveryError(error);
|
|
316
316
|
const deterministic = !transient && isDeterministicClientError(error);
|
|
317
|
-
//
|
|
317
|
+
// flush's forced loop hammers every 150ms while ignoring backoff; letting
|
|
318
318
|
// those passes consume the TRANSIENT attempt budget is what let a <2s relay restart
|
|
319
319
|
// window poison frames at a flush boundary. Forced drains of transient failures
|
|
320
320
|
// therefore burn NO attempt budget — classification still applies, counting does
|
|
@@ -328,7 +328,7 @@ export class Outbox {
|
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
if (transient) {
|
|
331
|
-
// Never poisoned on attempt count
|
|
331
|
+
// Never poisoned on attempt count: retry under backoff until either the
|
|
332
332
|
// relay returns (the row delivers) or the CONTINUOUS-failure wall-clock budget
|
|
333
333
|
// runs out and the row parks onto a slow periodic re-drive cadence.
|
|
334
334
|
const firstFailedAt = row.first_failed_at > 0 ? row.first_failed_at : now;
|
|
@@ -369,7 +369,7 @@ export class Outbox {
|
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
// Permanently drop a row. Only reachable for deterministic client rejections (immediately)
|
|
372
|
-
// or unknown-class failures past maxAttempts — NEVER for transient ones
|
|
372
|
+
// or unknown-class failures past maxAttempts — NEVER for transient ones. The
|
|
373
373
|
// onPoison seam lets the session outbox surface the loss in-session instead of only in
|
|
374
374
|
// this FATAL host log.
|
|
375
375
|
private async poisonRow(row: Row, attempts: number, reason: string, immediate: boolean): Promise<void> {
|
|
@@ -29,7 +29,7 @@ export class PendingPermissionStore {
|
|
|
29
29
|
readonly path: string;
|
|
30
30
|
|
|
31
31
|
constructor(options: { agentId: string; dir?: string }) {
|
|
32
|
-
const dir = options.dir ?? runnerOutboxDirFromEnv() ?? join(tmpdir(), "
|
|
32
|
+
const dir = options.dir ?? runnerOutboxDirFromEnv() ?? join(tmpdir(), "humwick-outbox");
|
|
33
33
|
this.path = options.dir === ":memory:" ? ":memory:" : join(dir, `pending-permissions-${safeName(options.agentId)}.sqlite`);
|
|
34
34
|
try {
|
|
35
35
|
if (this.path !== ":memory:") mkdirSync(dirname(this.path), { recursive: true });
|
package/src/profile-home.ts
CHANGED
|
@@ -32,7 +32,7 @@ export function profileAllowsHostGlobalInstructions(config: { agentProfile?: Run
|
|
|
32
32
|
*
|
|
33
33
|
* The predicate itself lives in the sdk ({@link sdkProjectSettingSourcesOn}) so the
|
|
34
34
|
* effective-profile survey and the v2 cell deriver stay in step with the runner's
|
|
35
|
-
* actual launch derivation
|
|
35
|
+
* actual launch derivation —.
|
|
36
36
|
*/
|
|
37
37
|
export function projectSettingSourcesOn(config: { agentProfile?: RunnerSpawnConfig["agentProfile"] }): boolean {
|
|
38
38
|
const profile = config.agentProfile;
|
|
@@ -46,14 +46,14 @@ export function projectSettingSources(config: { agentProfile?: RunnerSpawnConfig
|
|
|
46
46
|
return projectSettingSourcesOn(config) ? ["user", "project"] : ["user"];
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
//
|
|
49
|
+
// every runner-managed profiled launch runs in a Relay-owned home; `base:"host"` only
|
|
50
50
|
// changes which host classes an omitted Host field inherits (see hostSourcesFor). A profile-less
|
|
51
51
|
// launch is a user's own wrapped session and keeps the provider's native home.
|
|
52
52
|
function profileRequiresIsolatedHome(config: RunnerSpawnConfig): boolean {
|
|
53
53
|
return Boolean(config.agentProfile);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
//
|
|
56
|
+
// PURE: the Relay-owned provider config home path a launch WILL use, or undefined for a
|
|
57
57
|
// profile-less launch (the provider's normal home). The launch assembler computes the home this way
|
|
58
58
|
// to report it + build CLAUDE_CONFIG_DIR/skill/plugin paths without side effects;
|
|
59
59
|
// prepare*ProfileHome creates it at the same path at spawn time.
|
|
@@ -78,7 +78,7 @@ export function providerHomePathFor(provider: SpawnProvider, config: RunnerSpawn
|
|
|
78
78
|
// the repo's CLAUDE.md / AGENTS.md as behavioral instructions, NOT "don't
|
|
79
79
|
// operate here" — an isolated agent must still read/edit/execute in its own cwd.
|
|
80
80
|
//
|
|
81
|
-
//
|
|
81
|
+
// host-base launches skip the onboarding/theme bootstrap (an
|
|
82
82
|
// onboarded host config already answers those), but they do NOT dodge folder trust:
|
|
83
83
|
// --dangerously-skip-permissions does not bypass the trust gate, and a cwd the host
|
|
84
84
|
// config has never seen still renders the dialog and wedges the spawn. Trust is
|
|
@@ -91,20 +91,20 @@ const BOOTSTRAP_HOOKS = new Map<string, BootstrapHook>();
|
|
|
91
91
|
/**
|
|
92
92
|
* The ONE materialization seam for an instance-keyed provider home
|
|
93
93
|
* (`<root>/<provider>/<profile>/<instanceId>`) — every adapter that creates one MUST route
|
|
94
|
-
* through it (
|
|
94
|
+
* through it (the OpenCode family built its home by hand, so its homes carried no
|
|
95
95
|
* owner-marker and no tombstone reclaim, and the reaper skipped every one of them as
|
|
96
96
|
* "unmarked" forever). `identity` must be exactly the path segments of `target`: the reaper
|
|
97
97
|
* refuses a marker that does not describe its own directory.
|
|
98
98
|
*/
|
|
99
99
|
export function materializeProviderHome(target: string, identity: { provider: string; profile: string; instanceId: string; agentId?: string }, mode?: number): void {
|
|
100
|
-
//
|
|
100
|
+
// spawn-side reclaim — FIRST recover any quarantine tombstone the reaper left for
|
|
101
101
|
// this exact instanceId (atomic rename back to `target`), BEFORE creating a fresh home.
|
|
102
102
|
// This makes reconnection always safe (a reconnecting/resuming agent recovers its home)
|
|
103
103
|
// and lets a crash-stranded tombstone self-heal. A lost reclaim race just falls through
|
|
104
104
|
// to a fresh mkdir below — never a corrupt home.
|
|
105
105
|
reclaimProviderHomeTombstone(target);
|
|
106
106
|
mkdirSync(target, { recursive: true, ...(mode === undefined ? {} : { mode }) });
|
|
107
|
-
//
|
|
107
|
+
// stamp the owner-marker so the reaper can later reclaim this home if it becomes
|
|
108
108
|
// orphaned, but only ever on THIS host (cross-host safety).
|
|
109
109
|
writeProviderHomeMarker(target, identity);
|
|
110
110
|
}
|
|
@@ -132,23 +132,23 @@ function prepareProviderHome(provider: SpawnProvider, config: RunnerSpawnConfig)
|
|
|
132
132
|
export const prepareCodexProfileHome = (config: RunnerSpawnConfig) => prepareProviderHome("codex", config);
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
|
-
*
|
|
135
|
+
* every Claude launch, isolated or host-base, leaves this function with folder trust
|
|
136
136
|
* already granted in the config file it will read. `prepareProviderHome` returns undefined for
|
|
137
137
|
* a host-base profile (and for a profile-less spawn), which is exactly the class that used to
|
|
138
138
|
* be seeded nowhere and wedged on the trust dialog; `ensureClaudeWorkspaceTrust` therefore runs
|
|
139
139
|
* unconditionally, targeting the shared host config when there is no isolated home.
|
|
140
140
|
*
|
|
141
|
-
*
|
|
141
|
+
* provider-id-dynamic for the claude family: claude-b passes its own id so the
|
|
142
142
|
* isolated home, bootstrap hook, and auth links resolve against `~/.claude-b` (its manifest
|
|
143
143
|
* configDir), never `~/.claude`.
|
|
144
144
|
*/
|
|
145
145
|
export const prepareClaudeProfileHome = (config: RunnerSpawnConfig, provider: SpawnProvider = "claude"): ProviderHome | undefined => {
|
|
146
146
|
const home = prepareProviderHome(provider, config);
|
|
147
|
-
//
|
|
147
|
+
// review HIGH-1 — a dedicatedHome provider's host-base launch still reads the
|
|
148
148
|
// provider's OWN effective home (launch assembly exports CLAUDE_CONFIG_DIR at it), so
|
|
149
149
|
// folder trust must be seeded in THAT home's .claude.json, not the shared ~/.claude.json.
|
|
150
150
|
const trust = ensureClaudeWorkspaceTrust(config.cwd, home?.path ?? providerHostConfigHome(provider));
|
|
151
|
-
// A seed that did not land puts this launch straight back into the
|
|
151
|
+
// A seed that did not land puts this launch straight back into the wedge — CC opens
|
|
152
152
|
// its trust dialog and the spawn hangs until a human answers. The failure modes are all
|
|
153
153
|
// environmental (an unparseable or unwritable host config, an operator opt-out), so the fix
|
|
154
154
|
// cannot self-heal; it must at least say so instead of failing silently the way the missing
|
|
@@ -160,7 +160,7 @@ export const prepareClaudeProfileHome = (config: RunnerSpawnConfig, provider: Sp
|
|
|
160
160
|
};
|
|
161
161
|
|
|
162
162
|
function bootstrapCodexFirstRun(codexHome: string, config: RunnerSpawnConfig): void {
|
|
163
|
-
// Keep bootstrap to one config read/write while repairing auth links
|
|
163
|
+
// Keep bootstrap to one config read/write while repairing auth links.
|
|
164
164
|
const path = join(codexHome, "config.toml");
|
|
165
165
|
const current = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
166
166
|
const next = withCodexWorkspaceTrust(withTomlFeatureFlag(current, "apps", false), config);
|
|
@@ -176,7 +176,7 @@ function bootstrapClaudeFirstRun(claudeHome: string, config: RunnerSpawnConfig):
|
|
|
176
176
|
|
|
177
177
|
BOOTSTRAP_HOOKS.set("codex", bootstrapCodexFirstRun);
|
|
178
178
|
BOOTSTRAP_HOOKS.set("claude", bootstrapClaudeFirstRun);
|
|
179
|
-
//
|
|
179
|
+
// the claude-b provider runs the identical claude first-run bootstrap (relay
|
|
180
180
|
// manual + config seed) inside its own claude-b-keyed home.
|
|
181
181
|
BOOTSTRAP_HOOKS.set("claude-b", bootstrapClaudeFirstRun);
|
|
182
182
|
BOOTSTRAP_HOOKS.set("claude-api", bootstrapClaudeFirstRun);
|
|
@@ -190,7 +190,7 @@ export function providerSourceHome(provider: string): string {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
|
-
*
|
|
193
|
+
* the effective HOST config home a `dedicatedHome` provider (claude-b) launches
|
|
194
194
|
* from when it has no isolated managed home, or undefined for the primary provider
|
|
195
195
|
* (claude), whose host-base launches keep their historical behavior of falling back to
|
|
196
196
|
* the binary's own resolution ($CLAUDE_CONFIG_DIR ?? ~/.claude). Callers that need one
|
|
@@ -209,7 +209,7 @@ function ensureClaudeConfigSeed(claudeHome: string, config: RunnerSpawnConfig):
|
|
|
209
209
|
writeJsonAtomic(path, seed);
|
|
210
210
|
return;
|
|
211
211
|
}
|
|
212
|
-
//
|
|
212
|
+
// /#harden — the isolated home is instance-keyed and PERSISTS across resumes/reconnects, so a
|
|
213
213
|
// relaunch (e.g. auto-resume after a tmux/provider exit) reuses the existing `.claude.json` and
|
|
214
214
|
// never re-runs the absent-file seed above. Re-heal every prompt-gate key the seed would have
|
|
215
215
|
// applied on a fresh home (onboarding, theme, bypass-permissions, workspace-trust): a resumed
|
|
@@ -309,7 +309,7 @@ export async function hydrateClaudeHostConfig(config: RunnerSpawnConfig, provide
|
|
|
309
309
|
* Loads only the seed values and host-scope MCP declaration required by a Claude launch. This
|
|
310
310
|
* shared worker-pool seam gives registration projection and synchronous launch assembly the same
|
|
311
311
|
* snapshot without ever parsing the host's potentially huge config on the runner event loop.
|
|
312
|
-
*
|
|
312
|
+
* keyed by provider id so a claude-family provider (claude-b) reads ITS source home.
|
|
313
313
|
*/
|
|
314
314
|
export async function readClaudeHostConfig(provider: SpawnProvider = "claude"): Promise<Record<string, unknown> | undefined> {
|
|
315
315
|
const hostPath = join(providerSourceHome(provider), ".claude.json");
|
|
@@ -322,10 +322,10 @@ export async function readClaudeHostSettings(provider: SpawnProvider = "claude")
|
|
|
322
322
|
return runnerHostConfigOffload().run<{ path: string; kind: "settings" }, Record<string, unknown> | undefined>("runner-claude-host-config-read", { path: settingsPath, kind: "settings" });
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
//
|
|
325
|
+
// the host USER-scope MCP servers: the top-level `mcpServers` of `~/.claude.json`.
|
|
326
326
|
// These are the ambient host MCP a host-base Claude auto-loads on EVERY launch when NOT
|
|
327
327
|
// strict (user scope is global across all projects — callmux/qmd/gh on this host). Since
|
|
328
|
-
// the
|
|
328
|
+
// the wedge fix makes every managed launch `--strict-mcp-config` (so Claude ignores
|
|
329
329
|
// all on-disk config and never renders the project-`.mcp.json` enable modal), an
|
|
330
330
|
// `mcp.mode:"host"` profile must re-inject these explicitly into `--mcp-config` to keep
|
|
331
331
|
// honoring host mode. Deliberately NOT the project-cwd `.mcp.json` (project scope — the
|
|
@@ -356,7 +356,7 @@ function linkExistingAuthItems(sourceHome: string, targetHome: string, items: st
|
|
|
356
356
|
|
|
357
357
|
function linkExistingPath(source: string, target: string): boolean {
|
|
358
358
|
if (!existsSync(source) || existsSync(target)) return false;
|
|
359
|
-
//
|
|
359
|
+
// existsSync follows links; a reaped auth source leaves a dangling entry behind.
|
|
360
360
|
if (lstatSync(target, { throwIfNoEntry: false })?.isSymbolicLink()) unlinkSync(target);
|
|
361
361
|
symlinkSync(source, target);
|
|
362
362
|
return true;
|