@humwick/runner 0.0.0-stage → 0.130.0
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/LICENSE +167 -0
- package/agent-bin/agent-relay-browser +10 -0
- package/agent-bin/humwick +10 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
- package/hooks/fff-search-redirect/hooks.json +13 -0
- package/hooks/rg-r-guard/hooks.json +18 -0
- package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
- package/hooks/web-tools-redirect/hooks.json +13 -0
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
- package/node_modules/@humwick/offload/LICENSE +167 -0
- package/node_modules/@humwick/offload/README.md +57 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
- package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.js +80 -0
- package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
- package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/index.js +5 -0
- package/node_modules/@humwick/offload/dist/index.js.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
- package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/types.js +176 -0
- package/node_modules/@humwick/offload/dist/types.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.js +1 -0
- package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
- package/node_modules/@humwick/offload/package.json +46 -0
- package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
- package/node_modules/@humwick/offload/src/envelope.ts +92 -0
- package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
- package/node_modules/@humwick/offload/src/index.ts +4 -0
- package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
- package/node_modules/@humwick/offload/src/types.ts +235 -0
- package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
- package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
- package/node_modules/@humwick/offload/src/worker.ts +5 -0
- package/package.json +47 -4
- package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
- package/plugins/claude/.claude-plugin/plugin.json +8 -0
- package/plugins/claude/claude-statusline-probe.cjs +144 -0
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
- package/plugins/claude/hooks/gate-run-guard.sh +9 -0
- package/plugins/claude/hooks/gate-run-guard.ts +237 -0
- package/plugins/claude/hooks/hooks.json +158 -0
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
- package/plugins/claude/hooks/kill-guard.sh +341 -0
- package/plugins/claude/hooks/kill-guard.ts +337 -0
- package/plugins/claude/hooks/permission-request.sh +22 -0
- package/plugins/claude/hooks/post-compact.sh +10 -0
- package/plugins/claude/hooks/post-tool-use.sh +8 -0
- package/plugins/claude/hooks/pre-compact.sh +12 -0
- package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
- package/plugins/claude/hooks/relay-status.sh +373 -0
- package/plugins/claude/hooks/session-end.sh +26 -0
- package/plugins/claude/hooks/session-start.sh +31 -0
- package/plugins/claude/hooks/shell-trust.ts +28 -0
- package/plugins/claude/hooks/stop-failure.sh +47 -0
- package/plugins/claude/hooks/stop.sh +48 -0
- package/plugins/claude/hooks/subagent-guard.cjs +26 -0
- package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
- package/plugins/claude/hooks/subagent-start.sh +13 -0
- package/plugins/claude/hooks/subagent-stop.sh +19 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
- package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
- package/plugins/claude/monitors/monitors.json +7 -0
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
- package/plugins/claude/monitors/relay-monitor.ts +58 -0
- package/plugins/claude/skills/disconnect/SKILL.md +16 -0
- package/plugins/claude/skills/label/SKILL.md +23 -0
- package/plugins/claude/skills/message/SKILL.md +40 -0
- package/plugins/claude/skills/pair/SKILL.md +30 -0
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +44 -0
- package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
- package/plugins/claude/skills/slice-first/SKILL.md +32 -0
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/claude/skills/status/SKILL.md +16 -0
- package/plugins/claude/skills/tags/SKILL.md +25 -0
- package/plugins/claude/skills/task/SKILL.md +28 -0
- package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
- package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
- package/plugins/codex/skills/disconnect/SKILL.md +15 -0
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/label/SKILL.md +22 -0
- package/plugins/codex/skills/message/SKILL.md +40 -0
- package/plugins/codex/skills/pair/SKILL.md +29 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +44 -0
- package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
- package/plugins/codex/skills/slice-first/SKILL.md +32 -0
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/codex/skills/status/SKILL.md +15 -0
- package/plugins/codex/skills/tags/SKILL.md +24 -0
- package/plugins/codex/skills/task/SKILL.md +28 -0
- package/src/adapter.ts +1061 -0
- package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
- package/src/adapters/claude-api.ts +243 -0
- package/src/adapters/claude-context-command.ts +16 -0
- package/src/adapters/claude-credentials.ts +22 -0
- package/src/adapters/claude-delivery-lanes.ts +28 -0
- package/src/adapters/claude-delivery.ts +166 -0
- package/src/adapters/claude-effort.ts +94 -0
- package/src/adapters/claude-exit.ts +290 -0
- package/src/adapters/claude-headless-mirror.ts +88 -0
- package/src/adapters/claude-headless-refusal.ts +51 -0
- package/src/adapters/claude-headless.ts +617 -0
- package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
- package/src/adapters/claude-permission.ts +243 -0
- package/src/adapters/claude-prompt-gates.ts +269 -0
- package/src/adapters/claude-prompt-hook.ts +145 -0
- package/src/adapters/claude-session-capture.ts +293 -0
- package/src/adapters/claude-session-probe.ts +414 -0
- package/src/adapters/claude-shutdown.ts +190 -0
- package/src/adapters/claude-status-detectors.ts +426 -0
- package/src/adapters/claude-subagents.ts +93 -0
- package/src/adapters/claude-submit-confirmation.ts +287 -0
- package/src/adapters/claude-tmux.ts +548 -0
- package/src/adapters/claude-token-usage-capture.ts +253 -0
- package/src/adapters/claude-tool-policy.ts +165 -0
- package/src/adapters/claude-transcript-tail.ts +193 -0
- package/src/adapters/claude-transcript.ts +812 -0
- package/src/adapters/claude.ts +1097 -0
- package/src/adapters/codex-agent-message-capture.ts +189 -0
- package/src/adapters/codex-app-server.ts +70 -0
- package/src/adapters/codex-client.ts +362 -0
- package/src/adapters/codex-effective-config.ts +87 -0
- package/src/adapters/codex-launch-config.ts +153 -0
- package/src/adapters/codex-rollout-context.ts +272 -0
- package/src/adapters/codex-skill-discovery.ts +18 -0
- package/src/adapters/codex-token-usage-capture.ts +163 -0
- package/src/adapters/codex-tool-output.ts +53 -0
- package/src/adapters/codex-turn-outcome.ts +27 -0
- package/src/adapters/codex-version.ts +64 -0
- package/src/adapters/codex.ts +1442 -0
- package/src/adapters/incremental-file.ts +110 -0
- package/src/adapters/opencode-api.ts +71 -0
- package/src/adapters/opencode-context.ts +277 -0
- package/src/adapters/opencode-model-registry.ts +126 -0
- package/src/adapters/opencode-provider.ts +169 -0
- package/src/adapters/opencode-server-retry.ts +50 -0
- package/src/adapters/opencode-soft-steer.ts +302 -0
- package/src/adapters/opencode-spawn.ts +145 -0
- package/src/adapters/opencode-subscribe.ts +62 -0
- package/src/adapters/opencode-token-usage-capture.ts +187 -0
- package/src/adapters/opencode-tool-events.ts +302 -0
- package/src/adapters/opencode-tools-config.ts +17 -0
- package/src/adapters/opencode.ts +2264 -0
- package/src/adapters/project-shell.ts +80 -0
- package/src/adapters/provider-launch-config.ts +21 -0
- package/src/adapters/provider-quota-harvest.ts +127 -0
- package/src/adapters/service-access.ts +153 -0
- package/src/adapters/tmux-offload.ts +207 -0
- package/src/adapters/tool-summary.ts +214 -0
- package/src/agent-redact.ts +75 -0
- package/src/agent-runner/commands/dispatch.ts +185 -0
- package/src/agent-runner/commands/lifecycle.ts +84 -0
- package/src/agent-runner/commands/prompt.ts +284 -0
- package/src/agent-runner/credentials.ts +208 -0
- package/src/agent-runner/http-liveness.ts +85 -0
- package/src/agent-runner/lifecycle-milestones.ts +207 -0
- package/src/agent-runner/lifecycle-phase.ts +80 -0
- package/src/agent-runner/registration.ts +163 -0
- package/src/agent-runner/runner-context.ts +158 -0
- package/src/agent-runner/session-mirror.ts +278 -0
- package/src/agent-runner/task-claims.ts +116 -0
- package/src/agent-runner/token-usage-coverage.ts +54 -0
- package/src/agent-runner/turn-report.ts +269 -0
- package/src/api-budget-accounting.ts +91 -0
- package/src/area-memory.ts +255 -0
- package/src/artifact-path-materialize.ts +52 -0
- package/src/async-guard.ts +35 -0
- package/src/attachment-cache-sweep.ts +31 -0
- package/src/attachment-cache.ts +195 -0
- package/src/background-script-reconcile.ts +106 -0
- package/src/bounded-log.ts +59 -0
- package/src/bounded-spawn-sync.ts +18 -0
- package/src/busy-reconciler.ts +282 -0
- package/src/claim-tracker.ts +140 -0
- package/src/claude-settings.ts +167 -0
- package/src/claude-workspace-trust.ts +289 -0
- package/src/config.ts +547 -0
- package/src/context-boundary.ts +101 -0
- package/src/context-command.ts +151 -0
- package/src/context-probe-state.ts +6 -0
- package/src/continuation-archive.ts +179 -0
- package/src/control-server.ts +730 -0
- package/src/delivery-policy.ts +9 -0
- package/src/delivery-result.ts +24 -0
- package/src/effort-command.ts +125 -0
- package/src/empty-turn.ts +816 -0
- package/src/free-port.ts +25 -0
- package/src/fs-offload.ts +26 -0
- package/src/gate-run-config.ts +35 -0
- package/src/gate-run-lease-probe.ts +38 -0
- package/src/host-asset-plan.ts +450 -0
- package/src/host-claude-config-worker.ts +34 -0
- package/src/host-sources.ts +40 -0
- package/src/inbox-carrier.ts +7 -0
- package/src/index.ts +386 -0
- package/src/launch-assembly.ts +955 -0
- package/src/liveness.ts +105 -0
- package/src/logger.ts +108 -0
- package/src/mcp-outbox.ts +204 -0
- package/src/mcp-preflight.ts +86 -0
- package/src/mcp-source-selection.ts +54 -0
- package/src/mcp-tool-deferral.ts +35 -0
- package/src/mcp-vault-env.ts +25 -0
- package/src/message-body-config/config.ts +16 -0
- package/src/message-body-config.ts +1 -0
- package/src/mid-turn-delivery.ts +43 -0
- package/src/native-memory-lever.ts +329 -0
- package/src/native-self-resume.ts +244 -0
- package/src/outbox.ts +453 -0
- package/src/park-post.ts +17 -0
- package/src/pending-permission-store.ts +148 -0
- package/src/process-meta.ts +9 -0
- package/src/profile-home.ts +419 -0
- package/src/profile-projection.ts +411 -0
- package/src/project-hooks.ts +65 -0
- package/src/project-instructions.ts +119 -0
- package/src/provider-double.ts +59 -0
- package/src/provider-hold.ts +228 -0
- package/src/provider-home-marker.ts +3 -0
- package/src/provider-home-tombstone.ts +4 -0
- package/src/provider-input-queue.ts +134 -0
- package/src/provider-integration.ts +93 -0
- package/src/provider-processes.ts +17 -0
- package/src/provider-submit-failure.ts +15 -0
- package/src/providers.ts +35 -0
- package/src/provisioning.ts +703 -0
- package/src/question-decision.ts +18 -0
- package/src/quota.ts +22 -0
- package/src/rate-limit.ts +351 -0
- package/src/refused-prompt-store.ts +36 -0
- package/src/relay-injection-events.ts +161 -0
- package/src/relay-instructions.ts +107 -0
- package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
- package/src/relay-mcp-proxy.ts +704 -0
- package/src/relay-mcp.ts +583 -0
- package/src/reply-obligation-cache.ts +211 -0
- package/src/response-capture-report.ts +111 -0
- package/src/response-routing.ts +6 -0
- package/src/runner-core.ts +2955 -0
- package/src/runner-exit.ts +12 -0
- package/src/runner-helpers.ts +582 -0
- package/src/runner-insights.ts +105 -0
- package/src/runner-mcp-proxy.ts +46 -0
- package/src/runner-spawn-config.ts +68 -0
- package/src/runner-stale-scratch-sweep.ts +21 -0
- package/src/runner.ts +9 -0
- package/src/runtime-token-file.ts +45 -0
- package/src/runtime-token-remint.ts +56 -0
- package/src/service-access.ts +88 -0
- package/src/session-destroy.ts +27 -0
- package/src/session-insights.ts +118 -0
- package/src/session-scratch.ts +296 -0
- package/src/spawn-input.ts +147 -0
- package/src/startup-config-fetch.ts +48 -0
- package/src/subreaper.ts +113 -0
- package/src/template-resolver.ts +29 -0
- package/src/test-run-dedup.ts +435 -0
- package/src/transcript-context.ts +99 -0
- package/src/transport-errors.ts +48 -0
- package/src/turn-opener.ts +55 -0
- package/src/turn-reconcile-event.ts +88 -0
- package/src/vault-proxy-hop.ts +99 -0
- package/src/version.ts +44 -0
- package/src/web-tool-call-proxy.ts +128 -0
- package/src/web-tools.ts +59 -0
- package/README.md +0 -3
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// #2254 — Claude folder-trust seeding for EVERY launch, host-base included.
|
|
2
|
+
//
|
|
3
|
+
// THE BUG: relay seeded `projects[cwd].hasTrustDialogAccepted` only into an ISOLATED
|
|
4
|
+
// `CLAUDE_CONFIG_DIR` (profile-home's `ensureClaudeConfigSeed`). Host-base profiles and
|
|
5
|
+
// profile-less spawns got nothing, on the assumption that `--dangerously-skip-permissions`
|
|
6
|
+
// bypasses the trust gate. It does not: CC 2.1.233 still renders "Is this a project you
|
|
7
|
+
// created or one you trust?" and blocks the launch until a human answers, so those spawns
|
|
8
|
+
// wedged until reaped. It looked random only because host-base is the minority class.
|
|
9
|
+
//
|
|
10
|
+
// THE FIX, in three parts:
|
|
11
|
+
// 1. Seed into the config file CC ACTUALLY READS for this launch — the isolated
|
|
12
|
+
// `<home>/.claude.json` when the profile has one, otherwise the shared config
|
|
13
|
+
// (`$CLAUDE_CONFIG_DIR/.claude.json`, else `~/.claude.json`). Note the shared file is
|
|
14
|
+
// the HOME-level `~/.claude.json`, not `~/.claude/.claude.json`: `providerSourceHome`
|
|
15
|
+
// points at `~/.claude` because that is where the auth items live, but CC's config
|
|
16
|
+
// lives one level up unless CLAUDE_CONFIG_DIR redirects it.
|
|
17
|
+
// 2. Seed EVERY key CC might look the project up under: the lexical cwd, its realpath,
|
|
18
|
+
// and the git canonical root (a worktree's state is keyed on the checkout that owns
|
|
19
|
+
// the common git dir). Cheap insurance against a path-shape mismatch.
|
|
20
|
+
// 3. Touch ONLY the trust flags. The shared file is the operator's real config (session
|
|
21
|
+
// history, mcpServers, auth-adjacent state); an unparseable one is left strictly
|
|
22
|
+
// alone rather than reseeded, and every other key round-trips untouched.
|
|
23
|
+
//
|
|
24
|
+
// Concurrency: the shared file has many writers (parallel spawns, and CC itself). We take
|
|
25
|
+
// an O_EXCL lockfile with a bounded wait around read → patch → atomic rename, then re-read
|
|
26
|
+
// to verify the flags actually landed. A lock we cannot get is never allowed to block a
|
|
27
|
+
// spawn — we break it and proceed, because an unseeded launch wedges for a human.
|
|
28
|
+
//
|
|
29
|
+
// Deliberately NOT `CLAUDE_CODE_SANDBOXED`: that env var would suppress the dialog by
|
|
30
|
+
// telling CC it runs sandboxed, which is a lie about the launch and changes other CC
|
|
31
|
+
// behaviour. Trust is granted as data, in the file CC reads.
|
|
32
|
+
//
|
|
33
|
+
// The sync fs/subprocess calls here are spawn-time only (once per launch, before the
|
|
34
|
+
// provider starts) — they are not on any tick or transport hot path, and the seed must be
|
|
35
|
+
// on disk before CC opens the config, so an async variant would only add an ordering risk.
|
|
36
|
+
|
|
37
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
38
|
+
import { homedir } from "node:os";
|
|
39
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
40
|
+
import { isRecord } from "@humwick/sdk";
|
|
41
|
+
import { spawnSyncBounded } from "./bounded-spawn-sync";
|
|
42
|
+
import { claudeTrustSeedDisabledFromEnv } from "./config";
|
|
43
|
+
|
|
44
|
+
/** The per-project flags CC's trust gate honours. Both must be `true` or the dialog renders. */
|
|
45
|
+
export const CLAUDE_TRUST_FLAGS = ["hasTrustDialogAccepted", "hasCompletedProjectOnboarding"] as const;
|
|
46
|
+
|
|
47
|
+
const LOCK_SUFFIX = ".relay-trust.lock";
|
|
48
|
+
const LOCK_WAIT_MS = 2_000;
|
|
49
|
+
const LOCK_POLL_MS = 20;
|
|
50
|
+
const LOCK_STALE_MS = 10_000;
|
|
51
|
+
const GIT_COMMON_DIR_ARGS = ["rev-parse", "--path-format=absolute", "--git-common-dir"];
|
|
52
|
+
|
|
53
|
+
export type ClaudeTrustSeedSkip = "no-cwd" | "unparseable-config" | "seed-disabled" | "write-failed";
|
|
54
|
+
|
|
55
|
+
export type ClaudeTrustSeedResult = {
|
|
56
|
+
/** The config file CC will read for this launch. */
|
|
57
|
+
path: string;
|
|
58
|
+
/** Every project key seeded (or that would have been seeded, when skipped). */
|
|
59
|
+
keys: string[];
|
|
60
|
+
/** Whether the file was rewritten (false = already trusted, nothing to do). */
|
|
61
|
+
changed: boolean;
|
|
62
|
+
/** Re-read confirmation that all keys carry both flags. */
|
|
63
|
+
verified: boolean;
|
|
64
|
+
skipped?: ClaudeTrustSeedSkip;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Every path key CC might resolve this launch's project state under.
|
|
69
|
+
*
|
|
70
|
+
* `resolve(cwd)` is the lexical path relay hands the provider; `realpathSync` differs
|
|
71
|
+
* whenever any ancestor is a symlink; the git canonical root is what CC keys a WORKTREE's
|
|
72
|
+
* project state on (the checkout owning the common git dir), which is a different directory
|
|
73
|
+
* from the worktree cwd for every isolated-workspace spawn.
|
|
74
|
+
*/
|
|
75
|
+
export function claudeTrustProjectKeys(cwd: string | undefined): string[] {
|
|
76
|
+
if (!cwd) return [];
|
|
77
|
+
const lexical = resolve(cwd);
|
|
78
|
+
const keys = new Set<string>([lexical]);
|
|
79
|
+
try {
|
|
80
|
+
keys.add(realpathSync(lexical));
|
|
81
|
+
} catch {
|
|
82
|
+
// cwd may not exist yet on this host; the lexical key still stands.
|
|
83
|
+
}
|
|
84
|
+
const canonical = gitCanonicalRoot(lexical);
|
|
85
|
+
if (canonical) keys.add(canonical);
|
|
86
|
+
return [...keys];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The `.claude.json` this launch's Claude will read: the isolated profile home when the
|
|
91
|
+
* launch has one (relay sets CLAUDE_CONFIG_DIR to it), otherwise the shared host config.
|
|
92
|
+
*/
|
|
93
|
+
export function claudeLaunchConfigPath(isolatedHome?: string): string {
|
|
94
|
+
if (isolatedHome) return join(isolatedHome, ".claude.json");
|
|
95
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
96
|
+
return configDir ? join(configDir, ".claude.json") : join(homedir(), ".claude.json");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Ensures the trust flags for `cwd` (and its sibling keys) exist in the config CC reads.
|
|
101
|
+
* Idempotent, and a no-op write when the flags are already true.
|
|
102
|
+
*/
|
|
103
|
+
export function ensureClaudeWorkspaceTrust(cwd: string | undefined, isolatedHome?: string): ClaudeTrustSeedResult {
|
|
104
|
+
const path = claudeLaunchConfigPath(isolatedHome);
|
|
105
|
+
const keys = claudeTrustProjectKeys(cwd);
|
|
106
|
+
if (!keys.length) return { path, keys, changed: false, verified: true, skipped: "no-cwd" };
|
|
107
|
+
// The ONLY way to switch the seed off is an explicit operator opt-out. It is deliberately
|
|
108
|
+
// not keyed on anything ambient (an earlier NODE_ENV check would have silently disabled the
|
|
109
|
+
// seed for ephemeral Proof-of-Life instances, which carry NODE_ENV through the env
|
|
110
|
+
// allowlist). Test processes stay off the real host config by pointing CLAUDE_CONFIG_DIR at
|
|
111
|
+
// a fixture (scripts/test-preload-provider-home.ts), never by disabling this path.
|
|
112
|
+
if (claudeTrustSeedDisabledFromEnv()) return { path, keys, changed: false, verified: false, skipped: "seed-disabled" };
|
|
113
|
+
try {
|
|
114
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
115
|
+
} catch {
|
|
116
|
+
// A missing, unwritable config dir surfaces on the patch attempt below.
|
|
117
|
+
}
|
|
118
|
+
return withConfigLock(path, () => patchTrustKeys(path, keys));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Read → patch trust flags only → atomic rename → re-read verify.
|
|
122
|
+
function patchTrustKeys(path: string, keys: string[]): ClaudeTrustSeedResult {
|
|
123
|
+
const existing = readConfigObject(path);
|
|
124
|
+
if (existing === "unparseable") return { path, keys, changed: false, verified: false, skipped: "unparseable-config" };
|
|
125
|
+
const config = existing ?? {};
|
|
126
|
+
const projects = isRecord(config.projects) ? { ...config.projects } : {};
|
|
127
|
+
let changed = false;
|
|
128
|
+
for (const key of keys) {
|
|
129
|
+
const entry = isRecord(projects[key]) ? { ...projects[key] } : {};
|
|
130
|
+
for (const flag of CLAUDE_TRUST_FLAGS) {
|
|
131
|
+
if (entry[flag] !== true) {
|
|
132
|
+
entry[flag] = true;
|
|
133
|
+
changed = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
projects[key] = entry;
|
|
137
|
+
}
|
|
138
|
+
if (!changed) return { path, keys, changed: false, verified: true };
|
|
139
|
+
config.projects = projects;
|
|
140
|
+
try {
|
|
141
|
+
writeJsonPreservingMode(path, config);
|
|
142
|
+
} catch {
|
|
143
|
+
return { path, keys, changed: false, verified: false, skipped: "write-failed" };
|
|
144
|
+
}
|
|
145
|
+
return { path, keys, changed: true, verified: trustVerified(path, keys) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** `undefined` = no file yet (safe to create); `"unparseable"` = leave the file alone. */
|
|
149
|
+
function readConfigObject(path: string): Record<string, unknown> | "unparseable" | undefined {
|
|
150
|
+
if (!existsSync(path)) return undefined;
|
|
151
|
+
try {
|
|
152
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
153
|
+
return isRecord(parsed) ? parsed : "unparseable";
|
|
154
|
+
} catch {
|
|
155
|
+
return "unparseable";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function trustVerified(path: string, keys: string[]): boolean {
|
|
160
|
+
const config = readConfigObject(path);
|
|
161
|
+
if (!config || config === "unparseable") return false;
|
|
162
|
+
const projects = isRecord(config.projects) ? config.projects : {};
|
|
163
|
+
return keys.every((key) => {
|
|
164
|
+
const entry = projects[key];
|
|
165
|
+
return isRecord(entry) && CLAUDE_TRUST_FLAGS.every((flag) => entry[flag] === true);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Atomic: temp file in the SAME directory, then rename(2) — a crash mid-write can only
|
|
170
|
+
// orphan the temp file, never truncate the config. An existing file keeps its own mode.
|
|
171
|
+
function writeJsonPreservingMode(path: string, value: unknown): void {
|
|
172
|
+
const mode = fileMode(path) ?? 0o600;
|
|
173
|
+
const tmpPath = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
174
|
+
let renamed = false;
|
|
175
|
+
try {
|
|
176
|
+
writeFileSync(tmpPath, JSON.stringify(value, null, 2), { mode });
|
|
177
|
+
renameSync(tmpPath, path);
|
|
178
|
+
renamed = true;
|
|
179
|
+
} finally {
|
|
180
|
+
// A failed write/rename must not leave the temp file beside the operator's config.
|
|
181
|
+
if (!renamed) {
|
|
182
|
+
try {
|
|
183
|
+
rmSync(tmpPath, { force: true });
|
|
184
|
+
} catch {
|
|
185
|
+
// Nothing further to do — the leftover is inert.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function fileMode(path: string): number | undefined {
|
|
192
|
+
try {
|
|
193
|
+
return statSync(path).mode & 0o777;
|
|
194
|
+
} catch {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* O_EXCL lockfile with a bounded wait. Every exit path runs `fn` — a lock we cannot take
|
|
201
|
+
* (stale holder, unwritable dir, a peer that outlasts the wait) must not wedge a spawn,
|
|
202
|
+
* and the worst case of an unlocked patch is losing a concurrent writer's flag for one
|
|
203
|
+
* key, which the next launch re-adds.
|
|
204
|
+
*/
|
|
205
|
+
function withConfigLock<T>(path: string, fn: () => T): T {
|
|
206
|
+
const lockPath = `${path}${LOCK_SUFFIX}`;
|
|
207
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
208
|
+
for (;;) {
|
|
209
|
+
const fd = tryAcquire(lockPath);
|
|
210
|
+
if (fd !== undefined) return runUnderLock(fd, lockPath, fn);
|
|
211
|
+
const expired = Date.now() >= deadline;
|
|
212
|
+
if (expired || lockIsStale(lockPath)) {
|
|
213
|
+
// Break it: a holder that died mid-write, or one that outlasts our wait, must not
|
|
214
|
+
// wedge a spawn on a file whose only contested content is a set of `true` flags.
|
|
215
|
+
try {
|
|
216
|
+
rmSync(lockPath, { force: true });
|
|
217
|
+
} catch {
|
|
218
|
+
return fn();
|
|
219
|
+
}
|
|
220
|
+
const takeover = tryAcquire(lockPath);
|
|
221
|
+
if (takeover !== undefined) return runUnderLock(takeover, lockPath, fn);
|
|
222
|
+
if (expired) return fn();
|
|
223
|
+
}
|
|
224
|
+
Bun.sleepSync(LOCK_POLL_MS);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function runUnderLock<T>(fd: number, lockPath: string, fn: () => T): T {
|
|
229
|
+
try {
|
|
230
|
+
return fn();
|
|
231
|
+
} finally {
|
|
232
|
+
closeSync(fd);
|
|
233
|
+
try {
|
|
234
|
+
rmSync(lockPath, { force: true });
|
|
235
|
+
} catch {
|
|
236
|
+
// A lock we cannot remove goes stale and is broken by the next writer.
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function tryAcquire(lockPath: string): number | undefined {
|
|
242
|
+
try {
|
|
243
|
+
return openSync(lockPath, "wx", 0o600);
|
|
244
|
+
} catch {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function lockIsStale(lockPath: string): boolean {
|
|
250
|
+
try {
|
|
251
|
+
return Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS;
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The checkout that owns the common git dir — the main checkout even when `cwd` is a linked
|
|
259
|
+
* worktree. The server has a richer, cached, filesystem-first twin
|
|
260
|
+
* (`src/services/canonical-root.ts`); it is not imported here because the runner is a
|
|
261
|
+
* separate package and that module is wired into the relay server's own loop ledger. One
|
|
262
|
+
* bounded git call per launch is the cheaper seam for the runner.
|
|
263
|
+
*/
|
|
264
|
+
function gitCanonicalRoot(cwd: string): string | undefined {
|
|
265
|
+
try {
|
|
266
|
+
const result = spawnSyncBounded(["git", "-C", cwd, ...GIT_COMMON_DIR_ARGS], {
|
|
267
|
+
stdin: "ignore", stdout: "pipe", stderr: "ignore", env: gitChildEnv(),
|
|
268
|
+
});
|
|
269
|
+
if (result.exitCode !== 0) return undefined;
|
|
270
|
+
const commonDir = result.stdout.toString().trim();
|
|
271
|
+
// Only a real `.../.git` common dir names a checkout root. A submodule's gitdir
|
|
272
|
+
// (`/repo/.git/modules/sub`) and bare repos do not, and must not be hoisted.
|
|
273
|
+
if (!commonDir || basename(commonDir) !== ".git") return undefined;
|
|
274
|
+
return dirname(commonDir);
|
|
275
|
+
} catch {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* An inherited `GIT_DIR` / `GIT_WORK_TREE` / `GIT_COMMON_DIR` overrides `-C`, so a runner
|
|
282
|
+
* launched from inside a git operation (a hook, a rebase) would answer about THAT repo and
|
|
283
|
+
* seed trust for the wrong root. Strip them; everything else the child may need stays.
|
|
284
|
+
*/
|
|
285
|
+
function gitChildEnv(): Record<string, string | undefined> {
|
|
286
|
+
const { GIT_DIR, GIT_WORK_TREE, GIT_COMMON_DIR, ...rest } = process.env;
|
|
287
|
+
void GIT_DIR; void GIT_WORK_TREE; void GIT_COMMON_DIR;
|
|
288
|
+
return rest;
|
|
289
|
+
}
|