@humwick/runner 0.0.0-stage → 0.130.1
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 +42 -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
package/src/config.ts
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { relayEnv, writeChildEnv } from "@humwick/sdk/env";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir, hostname } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { DEFAULT_RELAY_URL, PROJECT_TOOLCHAIN_ENV, RUNNER_CONFIG_RETRY_TIMEOUT_MS, ReconnectionManager, errMessage, isRecord, localOrchestratorIdFromEnv, stringValue } from "@humwick/sdk";
|
|
6
|
+
import type { ProjectToolchain, WorkspaceMetadata } from "@humwick/sdk";
|
|
7
|
+
import { sanitizeFsName } from "@humwick/sdk/fs-name";
|
|
8
|
+
import { defaultApprovalModeForProvider, getManifest } from "@humwick/providers";
|
|
9
|
+
import { fetchStartupConfig, type StartupFetchDeps } from "./startup-config-fetch";
|
|
10
|
+
import { spawnSyncBounded } from "./bounded-spawn-sync";
|
|
11
|
+
import { ACTIVE_TURN_MESSAGE_DELIVERY_VALUES, type ActiveTurnMessageDelivery } from "./adapter";
|
|
12
|
+
|
|
13
|
+
interface ProviderConfig {
|
|
14
|
+
command: string;
|
|
15
|
+
defaultArgs: string[];
|
|
16
|
+
env: Record<string, string>;
|
|
17
|
+
pluginDirs: string[];
|
|
18
|
+
globalInstructionFiles?: string[];
|
|
19
|
+
defaultCapabilities: string[];
|
|
20
|
+
defaultApprovalMode: string;
|
|
21
|
+
defaultTags: string[];
|
|
22
|
+
chatCaptureMode: "final" | "full";
|
|
23
|
+
reasoningCapture?: boolean;
|
|
24
|
+
// #2539 Phase A — optional: absent resolves through the OWNING adapter's default (codex
|
|
25
|
+
// adapter → steer, others → defer). `"defer"` is the operational kill switch for soft-steer.
|
|
26
|
+
activeTurnMessageDelivery?: ActiveTurnMessageDelivery;
|
|
27
|
+
headless: {
|
|
28
|
+
tmuxPrefix: string;
|
|
29
|
+
shutdownTimeoutMs: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface GlobalRunnerConfig {
|
|
34
|
+
relayUrl: string;
|
|
35
|
+
token?: string;
|
|
36
|
+
defaultCwd: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface LoadedProviderConfig extends ProviderConfig {
|
|
40
|
+
path: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function humwickHome(): string {
|
|
44
|
+
return relayEnv(process.env, "AGENT_RELAY_HOME") || join(homedir(), ".agent-relay");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function logDirFromEnv(): string | undefined {
|
|
48
|
+
return relayEnv(process.env, "AGENT_RELAY_LOG_DIR");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function providersDir(home = humwickHome()): string {
|
|
52
|
+
return join(home, "providers");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DEFAULT_PROVIDER_ARGS: Record<string, string[]> = {
|
|
56
|
+
claude: ["--dangerously-skip-permissions"],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function defaultProviderConfig(provider: string): ProviderConfig {
|
|
60
|
+
const manifest = getManifest(provider);
|
|
61
|
+
const home = recordValue(manifest?.home);
|
|
62
|
+
return {
|
|
63
|
+
command: manifest?.probe?.command ?? provider,
|
|
64
|
+
defaultArgs: DEFAULT_PROVIDER_ARGS[provider] ?? [],
|
|
65
|
+
env: {},
|
|
66
|
+
pluginDirs: [],
|
|
67
|
+
globalInstructionFiles: stringArray(home.globalInstructionFiles) ?? [],
|
|
68
|
+
defaultCapabilities: ["chat", "code", "review"],
|
|
69
|
+
defaultApprovalMode: defaultApprovalModeForProvider(provider),
|
|
70
|
+
defaultTags: [],
|
|
71
|
+
chatCaptureMode: "final",
|
|
72
|
+
reasoningCapture: true,
|
|
73
|
+
// #2539 Phase A — deliberately NOT set here: absent resolves through the OWNING adapter's
|
|
74
|
+
// default (codex → steer, others → defer) at the steer policy seam, so this config module
|
|
75
|
+
// carries no provider-identity branch (lint:provider-contract).
|
|
76
|
+
headless: {
|
|
77
|
+
tmuxPrefix: `${provider}-relay`,
|
|
78
|
+
shutdownTimeoutMs: 10_000,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function loadGlobalConfig(home = humwickHome()): GlobalRunnerConfig {
|
|
84
|
+
const path = join(home, "config.json");
|
|
85
|
+
const parsed = readJson(path);
|
|
86
|
+
return {
|
|
87
|
+
relayUrl: stringValue(parsed.relayUrl) ?? relayUrlFromEnv() ?? DEFAULT_RELAY_URL,
|
|
88
|
+
token: stringValue(parsed.token) ?? relayTokenFromEnv(),
|
|
89
|
+
defaultCwd: stringValue(parsed.defaultCwd) ?? process.cwd(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function relayUrlFromEnv(): string | undefined {
|
|
94
|
+
return relayEnv(process.env, "AGENT_RELAY_URL");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// #1096 — override for the Codex hooks version gate (finding 4). An explicit version string
|
|
98
|
+
// bypasses the `codex --version` probe: deterministic in tests, and an operator escape hatch
|
|
99
|
+
// when the probe can't run. Empty string means "assume supported".
|
|
100
|
+
export function codexVersionOverrideFromEnv(): string | undefined {
|
|
101
|
+
return relayEnv(process.env, "AGENT_RELAY_CODEX_VERSION");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function textEnvOrFile(name: string): string | undefined {
|
|
105
|
+
const inline = relayEnv(process.env, name);
|
|
106
|
+
if (inline) return inline;
|
|
107
|
+
const file = relayEnv(process.env, `${name}_FILE`);
|
|
108
|
+
if (!file) return undefined;
|
|
109
|
+
try {
|
|
110
|
+
const text = readFileSync(file, "utf8");
|
|
111
|
+
return text.length > 0 ? text : undefined;
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function relayTokenFromEnv(): string | undefined {
|
|
118
|
+
return relayEnv(process.env, "AGENT_RELAY_TOKEN");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// #2254 — escape hatch for the per-launch Claude folder-trust seed. Off by default: an
|
|
122
|
+
// unseeded launch wedges on CC's trust dialog until a human answers, so this exists only for
|
|
123
|
+
// an operator who deliberately wants relay to keep its hands off a shared config. Never set
|
|
124
|
+
// it to protect a test — tests point CLAUDE_CONFIG_DIR at a fixture instead
|
|
125
|
+
// (scripts/test-preload-provider-home.ts), so the production path stays exercised.
|
|
126
|
+
export function claudeTrustSeedDisabledFromEnv(): boolean {
|
|
127
|
+
return relayEnv(process.env, "AGENT_RELAY_TRUST_SEED_DISABLED") === "1";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// #2950 slice flag: launch Claude with `--effort` and accept mid-session effort switches.
|
|
131
|
+
export function effortSwitchEnabledFromEnv(): boolean {
|
|
132
|
+
return relayEnv(process.env, "AGENT_RELAY_EFFORT_SWITCH") === "1";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function runtimeTokenProfileFromEnv(): string | undefined {
|
|
136
|
+
return relayEnv(process.env, "AGENT_RELAY_TOKEN_PROFILE");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function runtimeTokenJtiFromEnv(): string | undefined {
|
|
140
|
+
return relayEnv(process.env, "AGENT_RELAY_TOKEN_JTI");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function runtimeTokenExpiresAtFromEnv(): string | undefined {
|
|
144
|
+
return relayEnv(process.env, "AGENT_RELAY_TOKEN_EXPIRES_AT");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function runtimeTokenFileFromEnv(): string | undefined {
|
|
148
|
+
return relayEnv(process.env, "AGENT_RELAY_TOKEN_FILE");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function runtimeTokenExpiresAtFileFromEnv(): string | undefined {
|
|
152
|
+
return relayEnv(process.env, "AGENT_RELAY_TOKEN_EXPIRES_AT_FILE");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Deadline for a freshly spawned runner's INITIAL relay registration. Reconnects after the
|
|
156
|
+
// first registration retry forever (a long-lived agent rides out a relay blip), but the
|
|
157
|
+
// initial handshake is bounded: a runner that can never register (a token the relay rejects,
|
|
158
|
+
// an unreachable relay) must EXIT instead of hanging `active` forever — a hung runner is
|
|
159
|
+
// invisible to the orchestrator's exit detection, so the spawn-parent is never notified.
|
|
160
|
+
export function registrationTimeoutMsFromEnv(): number {
|
|
161
|
+
const parsed = Number(relayEnv(process.env, "AGENT_RELAY_REGISTRATION_TIMEOUT_MS"));
|
|
162
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 60_000;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export { NATIVE_SELF_RESUME_TIMEOUT_MS, MIN_NATIVE_SELF_RESUME_TIMEOUT_MS, nativeSelfResumeTimeoutMsFromEnv } from "@humwick/sdk";
|
|
166
|
+
|
|
167
|
+
// #2095 self-compact e2e test rig — one-shot forced context utilization. When set, the runner
|
|
168
|
+
// REPORTS a synthetic ContextState at this utilization (lifecycleState "working", source "estimate",
|
|
169
|
+
// confidence "estimated") so the server's REAL 10s lifecycle tick fires the REAL advisory + REAL
|
|
170
|
+
// forced clear-inject exactly once — the actual compaction path is never stubbed. Accepts a fraction
|
|
171
|
+
// (0.90) or a percentage (90); both normalize to a 0..1 fraction. Unparseable / out of range (<=0 or
|
|
172
|
+
// >1 after normalization) → undefined = feature OFF, and the runner's context reporting is UNCHANGED.
|
|
173
|
+
// The strict one-shot LATCH lives in runner-core (heartbeatRuntimeMeta + handleCommand), not here.
|
|
174
|
+
export function forceContextPctFromEnv(): number | undefined {
|
|
175
|
+
const raw = relayEnv(process.env, "AGENT_RELAY_FORCE_CONTEXT_PCT");
|
|
176
|
+
if (raw === undefined || raw.trim() === "") return undefined;
|
|
177
|
+
const parsed = Number(raw);
|
|
178
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
179
|
+
// Accept a fraction (0.90) or a percentage (90): a value above 1 is read as a percentage. This is a
|
|
180
|
+
// config-lever parse (the value may already be a fraction), NOT the quota percent path, so it does
|
|
181
|
+
// not use normalizePercentUnit's always-/100 adapter rule.
|
|
182
|
+
let fraction = parsed;
|
|
183
|
+
if (fraction > 1) fraction = fraction / 100;
|
|
184
|
+
if (!(fraction > 0) || fraction > 1) return undefined;
|
|
185
|
+
return fraction;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function agentProfileNameFromEnv(): string | undefined {
|
|
189
|
+
return relayEnv(process.env, "AGENT_RELAY_AGENT_PROFILE");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Explicit launch policy is independent of whether the named profile still exists. */
|
|
193
|
+
export function harnessSubagentsFromEnv(env: Record<string, string | undefined> = process.env): boolean | undefined {
|
|
194
|
+
const value = relayEnv(env, "AGENT_RELAY_HARNESS_SUBAGENTS");
|
|
195
|
+
return value === "1" ? true : value === "0" ? false : undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function agentProfileJsonFromEnv(): string | undefined {
|
|
199
|
+
return textEnvOrFile("AGENT_RELAY_AGENT_PROFILE_JSON");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function workspaceJsonFromEnv(): string | undefined {
|
|
203
|
+
return textEnvOrFile("AGENT_RELAY_WORKSPACE_JSON");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** #2947 — the toolchain the orchestrator launched this runner on, reported at registration; the relay validates it. */
|
|
207
|
+
export function projectToolchainFromEnv(): ProjectToolchain | undefined {
|
|
208
|
+
try {
|
|
209
|
+
const parsed: unknown = JSON.parse(relayEnv(process.env, PROJECT_TOOLCHAIN_ENV) ?? "");
|
|
210
|
+
return isRecord(parsed) && typeof parsed.project === "string" && Array.isArray(parsed.tools) ? parsed as unknown as ProjectToolchain : undefined;
|
|
211
|
+
} catch {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function relayInjectionEventsJsonFromEnv(): string | undefined {
|
|
217
|
+
return textEnvOrFile("AGENT_RELAY_INJECTION_EVENTS_JSON");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function promptFromEnv(): string | undefined {
|
|
221
|
+
return textEnvOrFile("AGENT_RELAY_PROMPT");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function systemPromptAppendFromEnv(): string | undefined {
|
|
225
|
+
return textEnvOrFile("AGENT_RELAY_SYSTEM_PROMPT_APPEND");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function tmuxSessionFromEnv(): string | undefined {
|
|
229
|
+
return relayEnv(process.env, "AGENT_RELAY_TMUX_SESSION");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function policyNameFromEnv(): string | undefined {
|
|
233
|
+
return relayEnv(process.env, "AGENT_RELAY_POLICY");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function spawnRequestIdFromEnv(): string | undefined {
|
|
237
|
+
return relayEnv(process.env, "AGENT_RELAY_SPAWN_REQUEST_ID");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function taskIdFromEnv(): number | undefined {
|
|
241
|
+
const parsed = Number(relayEnv(process.env, "AGENT_RELAY_TASK_ID"));
|
|
242
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function automationIdFromEnv(): string | undefined {
|
|
246
|
+
return relayEnv(process.env, "AGENT_RELAY_AUTOMATION_ID");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function automationRunIdFromEnv(): string | undefined {
|
|
250
|
+
return relayEnv(process.env, "AGENT_RELAY_AUTOMATION_RUN_ID");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function lifecycleFromEnv(): string | undefined {
|
|
254
|
+
return relayEnv(process.env, "AGENT_RELAY_LIFECYCLE");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function sessionDebugEnabled(): boolean {
|
|
258
|
+
return relayEnv(process.env, "AGENT_RELAY_SESSION_DEBUG") === "1";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function logLevelFromEnv(): string | undefined {
|
|
262
|
+
return relayEnv(process.env, "AGENT_RELAY_LOG_LEVEL");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function envSwitchOn(name: string): boolean {
|
|
266
|
+
return !["0", "false", "off"].includes((relayEnv(process.env, name) ?? "").trim().toLowerCase());
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function mcpProxyEnabledFromEnv(): boolean {
|
|
270
|
+
return envSwitchOn("AGENT_RELAY_MCP_PROXY");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** #3025 rollback switch for agent-redact secret redaction hooks. */
|
|
274
|
+
export function redactionEnabledFromEnv(): boolean {
|
|
275
|
+
return envSwitchOn("AGENT_RELAY_REDACT");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function runnerInfoFileFromEnv(): string | undefined {
|
|
279
|
+
return relayEnv(process.env, "AGENT_RELAY_RUNNER_INFO_FILE");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// #1514 — the owning orchestrator's state-home id, stamped onto each tmux session
|
|
283
|
+
// (@agent-relay-owner) so the wedged-session reaper only reaps sessions it owns.
|
|
284
|
+
export function tmuxOwnerIdFromEnv(): string | undefined {
|
|
285
|
+
return relayEnv(process.env, "AGENT_RELAY_TMUX_OWNER");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function runnerOutboxDirFromEnv(): string | undefined {
|
|
289
|
+
return relayEnv(process.env, "AGENT_RELAY_RUNNER_OUTBOX_DIR");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function runnerOutboxDirWithInfoFallback(): string | undefined {
|
|
293
|
+
const dir = runnerOutboxDirFromEnv();
|
|
294
|
+
if (dir) return dir;
|
|
295
|
+
const infoFile = runnerInfoFileFromEnv();
|
|
296
|
+
return infoFile ? join(dirname(infoFile), "outbox") : undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function capsFromEnv(): string | undefined {
|
|
300
|
+
return relayEnv(process.env, "AGENT_RELAY_CAPS");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function tagsFromEnv(): string | undefined {
|
|
304
|
+
return relayEnv(process.env, "AGENT_RELAY_TAGS");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function workspaceModeFromEnv(): string | undefined {
|
|
308
|
+
return relayEnv(process.env, "AGENT_RELAY_WORKSPACE_MODE");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function orchestratorBaseDirFromEnv(): string | undefined {
|
|
312
|
+
return relayEnv(process.env, "AGENT_RELAY_ORCHESTRATOR_BASE_DIR");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The orchestrator stamps its stable Relay id on every managed runner. Central
|
|
317
|
+
* provider configuration is keyed by that id, rather than the mutable OS hostname
|
|
318
|
+
* (for example, orchestrator `wbox` may report hostname `wbox-1`). Interactive
|
|
319
|
+
* runners have no owner id and retain the hostname-scoped fallback.
|
|
320
|
+
*/
|
|
321
|
+
export function providerConfigHostFromEnv(): string {
|
|
322
|
+
return localOrchestratorIdFromEnv() ?? hostname();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Stable managed key plus hostname-only compatibility read for legacy central rows. */
|
|
326
|
+
export function providerConfigLookupFromEnv(): { host: string; legacyHost?: string; managed: boolean } {
|
|
327
|
+
const managedHost = localOrchestratorIdFromEnv();
|
|
328
|
+
if (!managedHost) return { host: hostname(), managed: false };
|
|
329
|
+
const legacyHost = relayEnv(process.env, "AGENT_RELAY_ORCHESTRATOR_HOSTNAME")?.trim();
|
|
330
|
+
return {
|
|
331
|
+
host: managedHost,
|
|
332
|
+
...(legacyHost && legacyHost !== managedHost ? { legacyHost } : {}),
|
|
333
|
+
managed: true,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function runnerLogFileFromEnv(): string | undefined {
|
|
338
|
+
return typeof relayEnv(process.env, "AGENT_RELAY_LOG_FILE") === "string" ? relayEnv(process.env, "AGENT_RELAY_LOG_FILE") : undefined;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function orchestratorUrlFromEnv(): string | undefined {
|
|
342
|
+
return relayEnv(process.env, "AGENT_RELAY_ORCHESTRATOR_URL");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function contextStateDirFromEnv(): string | undefined {
|
|
346
|
+
return relayEnv(process.env, "AGENT_RELAY_CONTEXT_STATE_DIR");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function attachmentCacheDirFromEnv(): string | undefined {
|
|
350
|
+
return relayEnv(process.env, "AGENT_RELAY_ATTACHMENT_CACHE_DIR");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function providerHomeRootFromEnv(): string {
|
|
354
|
+
return relayEnv(process.env, "AGENT_RELAY_PROVIDER_HOME_ROOT") || join(homedir(), ".agent-relay", "provider-homes");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function loadProviderConfig(provider: string, home = humwickHome()): LoadedProviderConfig {
|
|
358
|
+
const path = join(providersDir(home), `${provider}.json`);
|
|
359
|
+
const raw = readJson(path);
|
|
360
|
+
const defaults = defaultProviderConfig(provider);
|
|
361
|
+
return {
|
|
362
|
+
path,
|
|
363
|
+
command: stringValue(raw.command) ?? defaults.command,
|
|
364
|
+
defaultArgs: stringArray(raw.defaultArgs) ?? defaults.defaultArgs,
|
|
365
|
+
env: stringRecord(raw.env) ?? defaults.env,
|
|
366
|
+
pluginDirs: stringArray(raw.pluginDirs) ?? defaults.pluginDirs,
|
|
367
|
+
globalInstructionFiles: stringArray(raw.globalInstructionFiles) ?? defaults.globalInstructionFiles,
|
|
368
|
+
defaultCapabilities: stringArray(raw.defaultCapabilities) ?? defaults.defaultCapabilities,
|
|
369
|
+
defaultApprovalMode: stringValue(raw.defaultApprovalMode) ?? defaults.defaultApprovalMode,
|
|
370
|
+
defaultTags: stringArray(raw.defaultTags) ?? defaults.defaultTags,
|
|
371
|
+
chatCaptureMode: enumValue(raw.chatCaptureMode, ["final", "full"]) ?? defaults.chatCaptureMode,
|
|
372
|
+
reasoningCapture: booleanValue(raw.reasoningCapture) ?? defaults.reasoningCapture,
|
|
373
|
+
// #2539 — an invalid value falls back to absent, which the steer policy resolves through
|
|
374
|
+
// the owning adapter's default.
|
|
375
|
+
activeTurnMessageDelivery: enumValue(raw.activeTurnMessageDelivery, [...ACTIVE_TURN_MESSAGE_DELIVERY_VALUES]),
|
|
376
|
+
headless: {
|
|
377
|
+
tmuxPrefix: stringValue(recordValue(raw.headless).tmuxPrefix) ?? defaults.headless.tmuxPrefix,
|
|
378
|
+
shutdownTimeoutMs: positiveInteger(recordValue(raw.headless).shutdownTimeoutMs) ?? defaults.headless.shutdownTimeoutMs,
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export async function loadProviderConfigFromRelay(
|
|
384
|
+
provider: string,
|
|
385
|
+
opts: { relayUrl: string; token?: string; home?: string; host?: string; legacyHost?: string; managed?: boolean },
|
|
386
|
+
): Promise<LoadedProviderConfig> {
|
|
387
|
+
const host = opts.host ?? hostname();
|
|
388
|
+
const home = opts.home ?? humwickHome();
|
|
389
|
+
const local = loadProviderConfig(provider, home);
|
|
390
|
+
const key = `${host}/${provider}`;
|
|
391
|
+
const attemptedKeys = [key];
|
|
392
|
+
const retry = opts.managed ? { deadlineAt: Date.now() + Math.min(registrationTimeoutMsFromEnv(), RUNNER_CONFIG_RETRY_TIMEOUT_MS), reconnect: new ReconnectionManager() } : undefined;
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const fetched = await fetchProviderConfig(provider, opts.relayUrl, opts.token, key, retry);
|
|
396
|
+
if (fetched && fetched.version > 0) return fetched.config;
|
|
397
|
+
if (opts.legacyHost) {
|
|
398
|
+
const legacyKey = `${opts.legacyHost}/${provider}`;
|
|
399
|
+
attemptedKeys.push(legacyKey);
|
|
400
|
+
const legacy = await fetchProviderConfig(provider, opts.relayUrl, opts.token, legacyKey, retry);
|
|
401
|
+
if (legacy && legacy.version > 0) {
|
|
402
|
+
console.warn(`[agent-relay] central provider config ${key} is missing; using legacy hostname key ${legacyKey}. Migrate this row to ${key}.`);
|
|
403
|
+
return legacy.config;
|
|
404
|
+
}
|
|
405
|
+
if (!fetched && legacy) return legacy.config;
|
|
406
|
+
}
|
|
407
|
+
if (fetched) return fetched.config;
|
|
408
|
+
if (opts.managed) throw new Error(`central provider config missing for managed keys ${attemptedKeys.join(", ")}`);
|
|
409
|
+
} catch (error) {
|
|
410
|
+
// #3238: a local fallback can select a different, obsolete provider binary.
|
|
411
|
+
if (opts.managed) throw new Error(`Managed provider config ${key} could not be loaded: ${errMessage(error)}`);
|
|
412
|
+
console.warn(`[agent-relay] provider config relay read failed (${errMessage(error)}); using local fallback ${local.path}.`);
|
|
413
|
+
}
|
|
414
|
+
return local;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function writeProviderConfig(provider: string, config: ProviderConfig, home = humwickHome()): LoadedProviderConfig {
|
|
418
|
+
const dir = providersDir(home);
|
|
419
|
+
mkdirSync(dir, { recursive: true });
|
|
420
|
+
const path = join(dir, `${provider}.json`);
|
|
421
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
422
|
+
chmodSync(path, 0o600);
|
|
423
|
+
return { ...config, path };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function runnerId(provider: string, cwd: string, label?: string): string {
|
|
427
|
+
const project = cwd.split("/").filter(Boolean).at(-1) || "workspace";
|
|
428
|
+
const cleanLabel = sanitizeFsName(label || project, { replacement: "-", lowercase: true });
|
|
429
|
+
return `${hostname()}-${provider}-${cleanLabel}-${crypto.randomUUID().slice(0, 8)}`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Stable project identifier for insights aggregation: the repo NAME, never a
|
|
434
|
+
* per-branch/per-session worktree dir or full path. Isolated workspace agents run
|
|
435
|
+
* from a session-specific worktree (…/workspaces/<repo>/<id>) whose basename is
|
|
436
|
+
* unique per session — using it would scatter one repo's data across many "projects"
|
|
437
|
+
* and break per-project rollups. So we resolve up to the main repo root, then take
|
|
438
|
+
* its basename. Falls back to the git toplevel of cwd (handles a direct agent
|
|
439
|
+
* launched in a subdir), then to the cwd basename for a non-git directory.
|
|
440
|
+
*/
|
|
441
|
+
export function resolveProjectName(cwd: string, workspace?: WorkspaceMetadata): string {
|
|
442
|
+
const root =
|
|
443
|
+
workspace?.repoRoot ||
|
|
444
|
+
workspace?.probe?.repoRoot ||
|
|
445
|
+
gitToplevel(cwd) ||
|
|
446
|
+
workspace?.sourceCwd ||
|
|
447
|
+
cwd;
|
|
448
|
+
return root.split("/").filter(Boolean).at(-1) || "unknown";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function gitToplevel(cwd: string): string | undefined {
|
|
452
|
+
try {
|
|
453
|
+
const result = spawnSyncBounded(["git", "-C", cwd, "rev-parse", "--show-toplevel"], {
|
|
454
|
+
stdin: "ignore", stdout: "pipe", stderr: "ignore",
|
|
455
|
+
});
|
|
456
|
+
if (result.exitCode !== 0) return undefined;
|
|
457
|
+
const out = result.stdout.toString().trim();
|
|
458
|
+
return out || undefined;
|
|
459
|
+
} catch {
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function resolveCwd(value: string | undefined, fallback: string): string {
|
|
465
|
+
return resolve(value || fallback);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function readJson(path: string): Record<string, unknown> {
|
|
469
|
+
if (!existsSync(path)) return {};
|
|
470
|
+
try {
|
|
471
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
472
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
|
|
473
|
+
} catch {
|
|
474
|
+
return {};
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function positiveInteger(value: unknown): number | undefined {
|
|
479
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function enumValue<T extends string>(value: unknown, allowed: T[]): T | undefined {
|
|
483
|
+
return typeof value === "string" && allowed.includes(value as T) ? (value as T) : undefined;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function booleanValue(value: unknown): boolean | undefined {
|
|
487
|
+
return typeof value === "boolean" ? value : undefined;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function stringArray(value: unknown): string[] | undefined {
|
|
491
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : undefined;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function stringRecord(value: unknown): Record<string, string> | undefined {
|
|
495
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
496
|
+
const entries = Object.entries(value);
|
|
497
|
+
if (entries.some(([, item]) => typeof item !== "string")) return undefined;
|
|
498
|
+
return Object.fromEntries(entries) as Record<string, string>;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function recordValue(value: unknown): Record<string, unknown> {
|
|
502
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function fetchProviderConfig(
|
|
506
|
+
provider: string,
|
|
507
|
+
relayUrl: string,
|
|
508
|
+
token: string | undefined,
|
|
509
|
+
key: string,
|
|
510
|
+
retry?: Partial<StartupFetchDeps>,
|
|
511
|
+
): Promise<{ config: LoadedProviderConfig; version: number } | null> {
|
|
512
|
+
const url = new URL(`/api/settings/provider-config/${encodeURIComponent(key)}`, relayUrl);
|
|
513
|
+
const { response: res, body } = await fetchStartupConfig(url, token, retry ? RUNNER_CONFIG_RETRY_TIMEOUT_MS : 0, retry);
|
|
514
|
+
if (res.status === 404) return null;
|
|
515
|
+
if (!res.ok) throw new Error(body?.error ? `${res.status}: ${body.error}` : `HTTP ${res.status}`);
|
|
516
|
+
if (!isRecord(body) || !Number.isSafeInteger(body.version) || body.version! < 0
|
|
517
|
+
|| !isRecord(body.value) || !stringValue(body.value.command)) {
|
|
518
|
+
throw new Error(`invalid central provider config response for ${key}`);
|
|
519
|
+
}
|
|
520
|
+
// Version zero is the hub's resolved default, not a failed config read (#3320).
|
|
521
|
+
return { config: providerConfigFromValue(provider, body.value, `config:provider-config/${key}`), version: body.version! };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function providerConfigFromValue(provider: string, value: unknown, path: string): LoadedProviderConfig {
|
|
525
|
+
const raw = recordValue(value);
|
|
526
|
+
const defaults = defaultProviderConfig(provider);
|
|
527
|
+
return {
|
|
528
|
+
path,
|
|
529
|
+
command: stringValue(raw.command) ?? defaults.command,
|
|
530
|
+
defaultArgs: stringArray(raw.defaultArgs) ?? defaults.defaultArgs,
|
|
531
|
+
env: stringRecord(raw.env) ?? defaults.env,
|
|
532
|
+
pluginDirs: stringArray(raw.pluginDirs) ?? defaults.pluginDirs,
|
|
533
|
+
globalInstructionFiles: stringArray(raw.globalInstructionFiles) ?? defaults.globalInstructionFiles,
|
|
534
|
+
defaultCapabilities: stringArray(raw.defaultCapabilities) ?? defaults.defaultCapabilities,
|
|
535
|
+
defaultApprovalMode: stringValue(raw.defaultApprovalMode) ?? defaults.defaultApprovalMode,
|
|
536
|
+
defaultTags: stringArray(raw.defaultTags) ?? defaults.defaultTags,
|
|
537
|
+
chatCaptureMode: enumValue(raw.chatCaptureMode, ["final", "full"]) ?? defaults.chatCaptureMode,
|
|
538
|
+
reasoningCapture: booleanValue(raw.reasoningCapture) ?? defaults.reasoningCapture,
|
|
539
|
+
// #2539 — an invalid value falls back to absent, which the steer policy resolves through
|
|
540
|
+
// the owning adapter's default.
|
|
541
|
+
activeTurnMessageDelivery: enumValue(raw.activeTurnMessageDelivery, [...ACTIVE_TURN_MESSAGE_DELIVERY_VALUES]),
|
|
542
|
+
headless: {
|
|
543
|
+
tmuxPrefix: stringValue(recordValue(raw.headless).tmuxPrefix) ?? defaults.headless.tmuxPrefix,
|
|
544
|
+
shutdownTimeoutMs: positiveInteger(recordValue(raw.headless).shutdownTimeoutMs) ?? defaults.headless.shutdownTimeoutMs,
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { errMessage } from "@humwick/sdk";
|
|
2
|
+
import type { ProviderStatusUpdate } from "./adapter";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* #3398 — Relay lists the agent's reads when a provider context resets. A reset fact goes out only after
|
|
6
|
+
* every delivery settlement started before it, so each mark-read the old context started has committed or
|
|
7
|
+
* failed by then; a failed one leaves its message unread.
|
|
8
|
+
*/
|
|
9
|
+
export class DeliverySettlements {
|
|
10
|
+
private readonly unsettled = new Set<Promise<void>>();
|
|
11
|
+
|
|
12
|
+
track(work: Promise<unknown>): void {
|
|
13
|
+
const settled: Promise<void> = work.then(() => undefined, () => undefined).finally(() => this.unsettled.delete(settled));
|
|
14
|
+
this.unsettled.add(settled);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Resolves once every settlement tracked so far has committed or failed. */
|
|
18
|
+
settled(): Promise<void> {
|
|
19
|
+
return Promise.all([...this.unsettled]).then(() => undefined);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ContextResetKind = "compact" | "clear";
|
|
24
|
+
|
|
25
|
+
/** A boundary command that gave up before its destructive input reached the provider (#3441). */
|
|
26
|
+
export class ContextSubmitAborted extends Error {}
|
|
27
|
+
|
|
28
|
+
export interface ContextResetFact {
|
|
29
|
+
resetId: string;
|
|
30
|
+
kind: ContextResetKind;
|
|
31
|
+
observedAt: number;
|
|
32
|
+
/** The boundary command whose destructive input this reset settled, on the provider's evidence. */
|
|
33
|
+
commandId?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* #3398 (docs/design/self-resume-carry.md) — one reset fact per genuine provider reset, waited for or not.
|
|
38
|
+
* Submitted destructive inputs stay in order until provider evidence settles one: a manual reset settles
|
|
39
|
+
* the oldest of its kind, an automatic one none. A timed-out command keeps input the provider holds.
|
|
40
|
+
*/
|
|
41
|
+
/** How long a reset the adapter confirmed may still be observed as its provider status (#3698). */
|
|
42
|
+
export const CONFIRMED_RESET_OBSERVE_WINDOW_MS = 60_000;
|
|
43
|
+
|
|
44
|
+
export class ContextResetReporter {
|
|
45
|
+
private readonly submitted: Array<{ commandId: string; kind: ContextResetKind }> = [];
|
|
46
|
+
/** #3698 — confirmed by the adapter before its provider status arrived: that status is the SAME reset. */
|
|
47
|
+
private readonly confirmedUnobserved: Array<{ commandId: string; kind: ContextResetKind; at: number }> = [];
|
|
48
|
+
|
|
49
|
+
/** `onUncommandedReset` runs at observation for a reset no boundary command caused (operator `/clear`, auto-compaction). */
|
|
50
|
+
constructor(private readonly settlements: DeliverySettlements, private readonly send: (fact: ContextResetFact) => void, private readonly log: (message: string) => void, private readonly onUncommandedReset: () => void = () => {}) {}
|
|
51
|
+
|
|
52
|
+
/** The destructive input of boundary `commandId` is about to reach the provider. */
|
|
53
|
+
submitting(commandId: string, kind: ContextResetKind): void {
|
|
54
|
+
this.submitted.push({ commandId, kind });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Submitted, and no reset has settled it yet. */
|
|
58
|
+
unsettled(commandId: string): boolean {
|
|
59
|
+
return this.submitted.some((entry) => entry.commandId === commandId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The adapter itself confirmed boundary `commandId`'s reset (a clear that rotated the session). */
|
|
63
|
+
confirmed(kind: ContextResetKind, commandId: string): void {
|
|
64
|
+
const index = this.submitted.findIndex((entry) => entry.commandId === commandId);
|
|
65
|
+
const at = Date.now();
|
|
66
|
+
if (index >= 0) {
|
|
67
|
+
this.submitted.splice(index, 1);
|
|
68
|
+
this.confirmedUnobserved.push({ commandId, kind, at });
|
|
69
|
+
}
|
|
70
|
+
this.report({ resetId: `${kind}:${commandId}`, kind, observedAt: at, commandId });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Reports `update` when it is a provider reset: the `compacted` / `context-cleared` timeline. */
|
|
74
|
+
observeStatus(update: ProviderStatusUpdate): void {
|
|
75
|
+
if (typeof update === "string") return;
|
|
76
|
+
const kind = update.timeline?.status === "compacted" ? "compact" : update.timeline?.status === "context-cleared" ? "clear" : undefined;
|
|
77
|
+
if (!kind) return;
|
|
78
|
+
const observedAt = typeof update.timeline?.timestamp === "number" ? update.timeline.timestamp : Date.now();
|
|
79
|
+
// A clear is always the slash command; a compaction is ours only on the provider's manual trigger.
|
|
80
|
+
const manual = kind === "clear" || update.metadata?.compactTrigger === "manual";
|
|
81
|
+
const settled = manual ? this.settleObserved(kind, observedAt) : undefined;
|
|
82
|
+
if (!settled) this.onUncommandedReset();
|
|
83
|
+
this.report({ resetId: settled ? `${kind}:${settled}` : crypto.randomUUID(), kind, observedAt, ...(settled ? { commandId: settled } : {}) });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The boundary command a manual reset settles: the oldest submitted one of its kind, else one
|
|
87
|
+
* the adapter already confirmed (same reset id, so the server records it once). */
|
|
88
|
+
private settleObserved(kind: ContextResetKind, observedAt: number): string | undefined {
|
|
89
|
+
const index = this.submitted.findIndex((entry) => entry.kind === kind);
|
|
90
|
+
if (index >= 0) return this.submitted.splice(index, 1)[0]!.commandId;
|
|
91
|
+
for (let i = this.confirmedUnobserved.length - 1; i >= 0; i -= 1) {
|
|
92
|
+
if (observedAt - this.confirmedUnobserved[i]!.at > CONFIRMED_RESET_OBSERVE_WINDOW_MS) this.confirmedUnobserved.splice(i, 1);
|
|
93
|
+
}
|
|
94
|
+
const confirmed = this.confirmedUnobserved.findIndex((entry) => entry.kind === kind);
|
|
95
|
+
return confirmed >= 0 ? this.confirmedUnobserved.splice(confirmed, 1)[0]!.commandId : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private report(fact: ContextResetFact): void {
|
|
99
|
+
void this.settlements.settled().then(() => this.send(fact)).catch((error) => this.log(`context reset fact not queued: ${errMessage(error)}`));
|
|
100
|
+
}
|
|
101
|
+
}
|