@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,88 @@
|
|
|
1
|
+
import { PROVIDER_TURN_WATCHDOG_TIMELINE_STATUS, RECONCILED_TURN_TIMELINE_STATUS } from "@humwick/sdk";
|
|
2
|
+
|
|
3
|
+
// Same shape the runner publishes (structurally identical to the local declarations in
|
|
4
|
+
// runner-core.ts / native-self-resume.ts, which is how the other timeline producers do it).
|
|
5
|
+
export interface RunnerTimelineEvent {
|
|
6
|
+
status: string;
|
|
7
|
+
id?: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
title?: string;
|
|
10
|
+
body?: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* #1621 — the announcement for a provider-turn claim the busy backstop force-cleared.
|
|
17
|
+
*
|
|
18
|
+
* Reconciliation is a real state change, not bookkeeping repair: the turn the coordinator is
|
|
19
|
+
* waiting on is over and produced no reply, and `busy` is exactly the signal it used to decide
|
|
20
|
+
* NOT to intervene. So the clear is EMITTED (the relay turns this event into an activity row plus
|
|
21
|
+
* an `agent.turn_interrupted` notification for the spawn parent) instead of silently correcting
|
|
22
|
+
* the card — a quietly-fixed card still leaves the coordinator's plan stalled on a turn that
|
|
23
|
+
* ended. Pure builder so the payload the parent's notice is derived from is unit-testable without
|
|
24
|
+
* standing up an AgentRunner.
|
|
25
|
+
*/
|
|
26
|
+
export function reconciledTurnTimelineEvent(input: {
|
|
27
|
+
turnId: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
startedAt: number;
|
|
30
|
+
now?: number;
|
|
31
|
+
}): RunnerTimelineEvent {
|
|
32
|
+
const now = input.now ?? Date.now();
|
|
33
|
+
return {
|
|
34
|
+
status: RECONCILED_TURN_TIMELINE_STATUS,
|
|
35
|
+
id: `provider-turn-reconciled-${input.turnId}`,
|
|
36
|
+
timestamp: now,
|
|
37
|
+
title: "Provider turn cleared by reconciliation",
|
|
38
|
+
body: `The provider turn ended without a turn-end signal (${input.reason}); the agent was observed idle at its prompt and is no longer busy.`,
|
|
39
|
+
icon: "ti-alert-triangle",
|
|
40
|
+
metadata: {
|
|
41
|
+
eventType: RECONCILED_TURN_TIMELINE_STATUS,
|
|
42
|
+
reconcileReason: input.reason,
|
|
43
|
+
turnId: input.turnId,
|
|
44
|
+
turnStartedAt: input.startedAt,
|
|
45
|
+
turnAgeMs: Math.max(0, now - input.startedAt),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* #1980 — the announcement for a live provider turn the busy backstop is HOLDING (not cancelling)
|
|
52
|
+
* past its prolonged-hold threshold: a frozen-active `shell` gate, or a pane-scrape-only long tool
|
|
53
|
+
* the runner cannot positively prove idle. In contrast to {@link reconciledTurnTimelineEvent} the
|
|
54
|
+
* turn is NOT ended and the card stays busy — this only surfaces "still working, held by the
|
|
55
|
+
* backstop" so a coordinator waiting on the turn knows it is alive rather than wedged-and-idle.
|
|
56
|
+
* Pure builder so the payload the parent's notice derives from is unit-testable in isolation.
|
|
57
|
+
*/
|
|
58
|
+
export function providerTurnWatchdogTimelineEvent(input: {
|
|
59
|
+
turnId: string;
|
|
60
|
+
reason: string;
|
|
61
|
+
startedAt: number;
|
|
62
|
+
now?: number;
|
|
63
|
+
}): RunnerTimelineEvent {
|
|
64
|
+
const now = input.now ?? Date.now();
|
|
65
|
+
return {
|
|
66
|
+
status: PROVIDER_TURN_WATCHDOG_TIMELINE_STATUS,
|
|
67
|
+
id: `provider-turn-watchdog-${input.turnId}`,
|
|
68
|
+
timestamp: now,
|
|
69
|
+
title: "Provider turn held by the busy backstop",
|
|
70
|
+
body: `The provider turn has been held ${describeWatchdogAge(Math.max(0, now - input.startedAt))} without a turn-end signal (${input.reason}); the busy backstop is holding it (NOT cancelling) because it cannot positively confirm the turn ended — most likely a long-running foreground tool. No action needed unless the pane looks genuinely stalled.`,
|
|
71
|
+
icon: "ti-clock-pause",
|
|
72
|
+
metadata: {
|
|
73
|
+
eventType: PROVIDER_TURN_WATCHDOG_TIMELINE_STATUS,
|
|
74
|
+
watchdogReason: input.reason,
|
|
75
|
+
turnId: input.turnId,
|
|
76
|
+
turnStartedAt: input.startedAt,
|
|
77
|
+
turnAgeMs: Math.max(0, now - input.startedAt),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function describeWatchdogAge(ageMs: number): string {
|
|
83
|
+
const minutes = Math.floor(ageMs / 60_000);
|
|
84
|
+
if (minutes < 1) return "under a minute";
|
|
85
|
+
if (minutes < 60) return `${minutes} min`;
|
|
86
|
+
const hours = Math.floor(minutes / 60);
|
|
87
|
+
return `${hours}h ${minutes % 60}m`;
|
|
88
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createServer, connect as netConnect, type Socket } from "node:net";
|
|
3
|
+
import { connect as tlsConnect } from "node:tls";
|
|
4
|
+
|
|
5
|
+
const MAX_HEAD_BYTES = 64 * 1024;
|
|
6
|
+
|
|
7
|
+
export interface VaultProxyHop {
|
|
8
|
+
/** Loopback proxy origin the provider is launched with; fixed for the launch. */
|
|
9
|
+
url: string;
|
|
10
|
+
/** Local credential the provider presents as proxy username; never valid at the Vault proxy. */
|
|
11
|
+
secret: string;
|
|
12
|
+
/** Swap the upstream session token; new connections use it, open tunnels keep theirs. */
|
|
13
|
+
setToken(token: string): void;
|
|
14
|
+
close(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function basic(user: string, password: string): string {
|
|
18
|
+
return `Basic ${Buffer.from(`${user}:${password}`).toString("base64")}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* #3577: a running provider cannot take a new proxy URL, and Agent Vault sessions cannot be extended.
|
|
23
|
+
* The provider therefore talks to this loopback hop, which forwards each request to the Vault proxy with
|
|
24
|
+
* the current session token. Agent Vault authenticates at CONNECT, so rotation never touches open tunnels.
|
|
25
|
+
*/
|
|
26
|
+
export async function startVaultProxyHop(upstreamUrl: string, vault: string, token: string, ca?: string[]): Promise<VaultProxyHop> {
|
|
27
|
+
const upstream = new URL(upstreamUrl);
|
|
28
|
+
const tls = upstream.protocol === "https:";
|
|
29
|
+
const port = Number(upstream.port || (tls ? 443 : 80));
|
|
30
|
+
const secret = randomBytes(24).toString("hex");
|
|
31
|
+
const expected = Buffer.from(basic(secret, vault));
|
|
32
|
+
let upstreamAuth = basic(token, vault);
|
|
33
|
+
const sockets = new Set<Socket>();
|
|
34
|
+
|
|
35
|
+
const forward = (client: Socket, head: string, rest: Buffer) => {
|
|
36
|
+
const [requestLine = "", ...headers] = head.split("\r\n");
|
|
37
|
+
const presented = Buffer.from(headers.find((line) => /^proxy-authorization:/i.test(line))?.slice(20).trim() ?? "");
|
|
38
|
+
if (presented.length !== expected.length || !timingSafeEqual(presented, expected)) {
|
|
39
|
+
client.end("HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"agent-relay\"\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const tunnel = /^CONNECT\s/i.test(requestLine);
|
|
43
|
+
// A forwarded plain request closes after its response, so a reused socket never reaches the Vault
|
|
44
|
+
// proxy without the rewritten credential.
|
|
45
|
+
const kept = headers.filter((line) => !/^proxy-authorization:/i.test(line) && (tunnel || !/^(?:proxy-)?connection:/i.test(line)));
|
|
46
|
+
const rewritten = [requestLine, ...kept, `Proxy-Authorization: ${upstreamAuth}`, ...(tunnel ? [] : ["Connection: close"]), "", ""].join("\r\n");
|
|
47
|
+
const target = tls
|
|
48
|
+
? tlsConnect({ host: upstream.hostname, port, servername: upstream.hostname, ...(ca ? { ca } : {}) })
|
|
49
|
+
: netConnect({ host: upstream.hostname, port });
|
|
50
|
+
sockets.add(target);
|
|
51
|
+
target.once(tls ? "secureConnect" : "connect", () => {
|
|
52
|
+
target.write(rewritten, "latin1");
|
|
53
|
+
if (rest.length) target.write(rest);
|
|
54
|
+
client.pipe(target);
|
|
55
|
+
target.pipe(client);
|
|
56
|
+
client.resume();
|
|
57
|
+
});
|
|
58
|
+
target.on("error", () => client.destroy());
|
|
59
|
+
target.on("close", () => { sockets.delete(target); client.destroy(); });
|
|
60
|
+
client.on("close", () => target.destroy());
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const server = createServer((client) => {
|
|
64
|
+
sockets.add(client);
|
|
65
|
+
client.on("close", () => sockets.delete(client));
|
|
66
|
+
client.on("error", () => client.destroy());
|
|
67
|
+
let head = Buffer.alloc(0);
|
|
68
|
+
const onData = (chunk: Buffer) => {
|
|
69
|
+
head = Buffer.concat([head, chunk]);
|
|
70
|
+
const end = head.indexOf("\r\n\r\n");
|
|
71
|
+
if (end < 0) {
|
|
72
|
+
if (head.length > MAX_HEAD_BYTES) client.destroy();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
client.off("data", onData);
|
|
76
|
+
client.pause();
|
|
77
|
+
forward(client, head.subarray(0, end).toString("latin1"), head.subarray(end + 4));
|
|
78
|
+
};
|
|
79
|
+
client.on("data", onData);
|
|
80
|
+
});
|
|
81
|
+
await new Promise<void>((resolve, reject) => {
|
|
82
|
+
server.once("error", reject);
|
|
83
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
84
|
+
});
|
|
85
|
+
// The provider's lifetime, not this listener, keeps the runner alive.
|
|
86
|
+
server.unref();
|
|
87
|
+
const address = server.address();
|
|
88
|
+
if (!address || typeof address === "string") throw new Error("vault_proxy_hop_unavailable");
|
|
89
|
+
return {
|
|
90
|
+
url: `http://127.0.0.1:${address.port}`,
|
|
91
|
+
secret,
|
|
92
|
+
setToken(next) { upstreamAuth = basic(next, vault); },
|
|
93
|
+
close() {
|
|
94
|
+
server.close();
|
|
95
|
+
for (const socket of sockets) socket.destroy();
|
|
96
|
+
sockets.clear();
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { CONTRACT_VERSIONS } from "@humwick/sdk";
|
|
5
|
+
import { getManifest } from "@humwick/providers";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8")) as { name?: string; version?: string };
|
|
9
|
+
|
|
10
|
+
export const PACKAGE_NAME = pkg.name || "@humwick/runner";
|
|
11
|
+
export const VERSION = pkg.version || "0.0.0";
|
|
12
|
+
// Protocol versions are owned by the SDK (CONTRACT_VERSIONS) — derive, never redeclare.
|
|
13
|
+
export const RUNNER_PROTOCOL_VERSION = CONTRACT_VERSIONS.runnerProtocol;
|
|
14
|
+
export const PROVIDER_PLUGIN_PROTOCOL_VERSION = CONTRACT_VERSIONS.providerPluginProtocol;
|
|
15
|
+
|
|
16
|
+
export const CONTRACTS = {
|
|
17
|
+
runnerProtocol: RUNNER_PROTOCOL_VERSION,
|
|
18
|
+
providerPluginProtocol: PROVIDER_PLUGIN_PROTOCOL_VERSION,
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export const RUNTIME_CAPABILITIES = {
|
|
22
|
+
relayCommandBus: true,
|
|
23
|
+
managedRegistration: true,
|
|
24
|
+
lifecycleCommands: true,
|
|
25
|
+
claimRenewal: true,
|
|
26
|
+
deliveryRetry: true,
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export function runtimeMetadata(provider?: string) {
|
|
30
|
+
const manifest = provider ? getManifest(provider) : undefined;
|
|
31
|
+
return {
|
|
32
|
+
package: {
|
|
33
|
+
name: PACKAGE_NAME,
|
|
34
|
+
version: VERSION,
|
|
35
|
+
},
|
|
36
|
+
contracts: CONTRACTS,
|
|
37
|
+
capabilities: {
|
|
38
|
+
...RUNTIME_CAPABILITIES,
|
|
39
|
+
...(manifest?.provisioning?.bundledPlugin ? { bundledPlugin: true } : {}),
|
|
40
|
+
...(manifest?.provisioning?.bundledSkills ? { bundledSkills: true } : {}),
|
|
41
|
+
...(manifest?.provisioning?.bundledHooks?.length ? { bundledHooks: true } : {}),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { errMessage, isRecord, sharedCallmuxToolCallTimeoutMsFromEnv, type WebToolCallObservation } from "@humwick/sdk";
|
|
2
|
+
|
|
3
|
+
export interface WebToolLogOptions {
|
|
4
|
+
upstreamUrl: string;
|
|
5
|
+
search?(args: Record<string, unknown>, id: unknown, signal?: AbortSignal): Promise<Response>;
|
|
6
|
+
record(row: WebToolCallObservation): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const WEB_CALLS: Record<string, { tool: "search" | "fetch"; backend: string; input: string }> = {
|
|
10
|
+
brave__brave_web_search: { tool: "search", backend: "brave", input: "query" },
|
|
11
|
+
brave__brave_llm_context: { tool: "search", backend: "brave", input: "query" },
|
|
12
|
+
fetch__fetch: { tool: "fetch", backend: "fetch", input: "url" },
|
|
13
|
+
jina__read_url: { tool: "fetch", backend: "jina", input: "url" },
|
|
14
|
+
};
|
|
15
|
+
const MAX_RESULT_BYTES = 8 * 1024 * 1024;
|
|
16
|
+
|
|
17
|
+
async function boundedBody(response: Response): Promise<Uint8Array<ArrayBuffer>> {
|
|
18
|
+
const reader = response.body?.getReader();
|
|
19
|
+
if (!reader) return new Uint8Array();
|
|
20
|
+
const chunks: Uint8Array[] = [];
|
|
21
|
+
let size = 0;
|
|
22
|
+
try {
|
|
23
|
+
for (;;) {
|
|
24
|
+
const { value, done } = await reader.read();
|
|
25
|
+
if (done) break;
|
|
26
|
+
size += value.length;
|
|
27
|
+
if (size > MAX_RESULT_BYTES) throw new Error("WEB_RESULT_TOO_LARGE");
|
|
28
|
+
chunks.push(value);
|
|
29
|
+
}
|
|
30
|
+
} catch (error) { await reader.cancel().catch(() => {}); throw error; }
|
|
31
|
+
finally { reader.releaseLock(); }
|
|
32
|
+
const body = new Uint8Array(size);
|
|
33
|
+
let offset = 0;
|
|
34
|
+
for (const chunk of chunks) { body.set(chunk, offset); offset += chunk.length; }
|
|
35
|
+
return body;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function rpcOutcome(body: Uint8Array, sse: boolean): { status: string; error: string | null; resultSize: number; cacheHit: boolean | null } {
|
|
39
|
+
const text = new TextDecoder().decode(body);
|
|
40
|
+
const frames = sse ? text.split(/\r?\n\r?\n/).map(frame => frame.split(/\r?\n/).filter(line => line.startsWith("data:")).map(line => line.slice(5).trimStart()).join("\n")).filter(Boolean) : [text];
|
|
41
|
+
let rpc: Record<string, unknown> | undefined;
|
|
42
|
+
for (const frame of frames) {
|
|
43
|
+
const parsed: unknown = JSON.parse(frame);
|
|
44
|
+
if (isRecord(parsed) && ("result" in parsed || "error" in parsed)) rpc = parsed;
|
|
45
|
+
}
|
|
46
|
+
if (!rpc) throw new Error("INVALID_MCP_RESPONSE");
|
|
47
|
+
const result = isRecord(rpc.result) ? rpc.result : {};
|
|
48
|
+
const structured = isRecord(result.structuredContent) ? result.structuredContent : {};
|
|
49
|
+
const failure = isRecord(rpc.error) ? rpc.error : result.isError ? isRecord(structured.error) ? structured.error : result : undefined;
|
|
50
|
+
const meta = isRecord(result._meta) ? result._meta : {};
|
|
51
|
+
const error = failure ? (typeof failure.message === "string" ? failure.message : JSON.stringify(failure.content ?? failure)).slice(0, 1000) : null;
|
|
52
|
+
return {
|
|
53
|
+
status: failure ? typeof failure.code === "number" || typeof failure.code === "string" ? String(failure.code) : "TOOL_ERROR" : "success",
|
|
54
|
+
error, resultSize: new TextEncoder().encode(JSON.stringify(rpc.result ?? rpc.error)).length,
|
|
55
|
+
cacheHit: typeof meta.cacheHit === "boolean" ? meta.cacheHit : null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The shared bridge and meta-only downstream both cross this per-agent seam. */
|
|
60
|
+
export async function forwardSharedMcp(req: Request, options: WebToolLogOptions): Promise<Response> {
|
|
61
|
+
const text = req.method === "POST" ? await req.text() : undefined;
|
|
62
|
+
let rpc: Record<string, unknown> | undefined;
|
|
63
|
+
let call: Record<string, unknown> | undefined;
|
|
64
|
+
try { const parsed = JSON.parse(text ?? "null"); if (isRecord(parsed)) rpc = parsed; if (rpc?.method === "tools/call" && isRecord(rpc.params)) call = rpc.params; } catch {}
|
|
65
|
+
const spec = typeof call?.name === "string" ? WEB_CALLS[call.name] : undefined;
|
|
66
|
+
const args = isRecord(call?.arguments) ? call.arguments : {};
|
|
67
|
+
if (call?.name === "web_search" || spec?.tool === "search") {
|
|
68
|
+
if (!options.search) return Response.json({ jsonrpc: "2.0", id: rpc?.id, error: { code: -32000, message: "Relay web_search is unavailable" } });
|
|
69
|
+
return options.search({ query: args.query, ...(args.count !== undefined ? { count: args.count } : {}) }, rpc?.id, req.signal);
|
|
70
|
+
}
|
|
71
|
+
const time = Date.now();
|
|
72
|
+
const started = performance.now();
|
|
73
|
+
const row: WebToolCallObservation | undefined = spec ? {
|
|
74
|
+
id: crypto.randomUUID(), time, tool: spec.tool, backend: spec.backend,
|
|
75
|
+
input: String(args[spec.input] ?? "").slice(0, 16_000), status: "success", error: null,
|
|
76
|
+
durationMs: 0, resultSize: 0, cacheHit: null, fallbackChain: [spec.backend],
|
|
77
|
+
} : undefined;
|
|
78
|
+
let response: Response;
|
|
79
|
+
try {
|
|
80
|
+
const headers = new Headers();
|
|
81
|
+
for (const key of ["accept", "content-type", "mcp-session-id", "mcp-protocol-version", "x-callmux-cwd", "last-event-id"]) {
|
|
82
|
+
const value = req.headers.get(key); if (value) headers.set(key, value);
|
|
83
|
+
}
|
|
84
|
+
response = await fetch(options.upstreamUrl, {
|
|
85
|
+
method: req.method, headers, body: text,
|
|
86
|
+
signal: AbortSignal.any([req.signal, AbortSignal.timeout(sharedCallmuxToolCallTimeoutMsFromEnv())]),
|
|
87
|
+
// Bun's idle watchdog must not preempt the shared MCP call budget.
|
|
88
|
+
timeout: false,
|
|
89
|
+
} as RequestInit & { timeout: false });
|
|
90
|
+
if (!row) {
|
|
91
|
+
if (rpc?.method === "tools/list" && response.ok) return neutralSearchManifest(response);
|
|
92
|
+
return response;
|
|
93
|
+
}
|
|
94
|
+
const body = await boundedBody(response);
|
|
95
|
+
Object.assign(row, response.ok ? rpcOutcome(body, response.headers.get("content-type")?.includes("text/event-stream") ?? false)
|
|
96
|
+
: { status: `HTTP_${response.status}`, error: new TextDecoder().decode(body).slice(0, 1000), resultSize: body.length });
|
|
97
|
+
const outputHeaders = new Headers(response.headers);
|
|
98
|
+
outputHeaders.delete("content-encoding"); outputHeaders.delete("content-length");
|
|
99
|
+
response = new Response(body, { status: response.status, headers: outputHeaders });
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (!row) throw error;
|
|
102
|
+
row.status = isRecord(error) && typeof error.code === "string" ? error.code : error instanceof Error && error.name === "TimeoutError" ? "TIMEOUT" : "TRANSPORT_ERROR";
|
|
103
|
+
row.error = errMessage(error).slice(0, 1000);
|
|
104
|
+
response = Response.json({ jsonrpc: "2.0", id: null, error: { code: -32000, message: row.error } }, { status: 502 });
|
|
105
|
+
}
|
|
106
|
+
row!.durationMs = Math.max(0, Math.round(performance.now() - started));
|
|
107
|
+
try { await options.record(row!); }
|
|
108
|
+
catch (error) {
|
|
109
|
+
return Response.json({ jsonrpc: "2.0", id: null, error: { code: -32000, message: `WEB_CALL_LOG_FAILED: ${errMessage(error)}` } }, { status: 503 });
|
|
110
|
+
}
|
|
111
|
+
return response;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function neutralSearchManifest(response: Response): Promise<Response> {
|
|
115
|
+
const body = new TextDecoder().decode(await boundedBody(response));
|
|
116
|
+
const rewrite = (text: string) => {
|
|
117
|
+
const rpc = JSON.parse(text);
|
|
118
|
+
if (isRecord(rpc.result) && Array.isArray(rpc.result.tools)) {
|
|
119
|
+
rpc.result.tools = rpc.result.tools.filter((tool: { name?: string }) => !tool.name?.startsWith("brave__") && tool.name !== "web_search");
|
|
120
|
+
}
|
|
121
|
+
return JSON.stringify(rpc);
|
|
122
|
+
};
|
|
123
|
+
const output = response.headers.get("content-type")?.includes("text/event-stream")
|
|
124
|
+
? body.replace(/^data: (.+)$/gm, (_, text) => `data: ${rewrite(text)}`) : rewrite(body);
|
|
125
|
+
const headers = new Headers(response.headers);
|
|
126
|
+
headers.delete("content-encoding"); headers.delete("content-length");
|
|
127
|
+
return new Response(output, { status: response.status, headers });
|
|
128
|
+
}
|
package/src/web-tools.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { writeChildEnv } from "@humwick/sdk/env";
|
|
2
|
+
import { profileWebToolEnabled, sharedWebToolNames, WEB_TOOL_NAMES, WEB_TOOLS_ENV, type WebToolName } from "@humwick/sdk/web-tools";
|
|
3
|
+
import { webSearchEnabledFromEnv, SHARED_MCP_SERVER_NAME, type ProvisioningMcpServer } from "@humwick/sdk";
|
|
4
|
+
import type { RunnerSpawnConfig } from "./adapter";
|
|
5
|
+
import { profileAllowsRelayFeature, webToolsSteer } from "./relay-instructions";
|
|
6
|
+
|
|
7
|
+
// The legacy webTools.brave switch controls Relay search; credentials stay on the hub.
|
|
8
|
+
|
|
9
|
+
type WebToolsConfig = Pick<RunnerSpawnConfig, "agentProfile">;
|
|
10
|
+
|
|
11
|
+
/** Search uses Relay MCP; readers use the shared bridge. */
|
|
12
|
+
export function launchWebTools(config: WebToolsConfig, env: Record<string, string | undefined> = process.env): WebToolName[] {
|
|
13
|
+
return WEB_TOOL_NAMES.filter((name) => profileWebToolEnabled(config.agentProfile, name)
|
|
14
|
+
&& (name === "brave" ? profileAllowsRelayFeature(config, "mcp") && webSearchEnabledFromEnv(env) : Boolean(config.agentProfile?.sharedMcp)));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Shared-listener tool names the profile switched off. */
|
|
18
|
+
export function disabledSharedWebTools(config: WebToolsConfig): string[] {
|
|
19
|
+
if (!config.agentProfile?.sharedMcp) return [];
|
|
20
|
+
const disabled = WEB_TOOL_NAMES.filter((name) => !profileWebToolEnabled(config.agentProfile, name)).flatMap(sharedWebToolNames);
|
|
21
|
+
return disabled;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function claudeDisallowedWebTools(config: WebToolsConfig): string[] {
|
|
25
|
+
return disabledSharedWebTools(config).map((tool) => `mcp__${SHARED_MCP_SERVER_NAME}__${tool}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Claude reads its steer from the launch system prompt: the monitor's context only arrives with a Relay message. */
|
|
29
|
+
export function claudeWebToolsSteer(config: WebToolsConfig): string {
|
|
30
|
+
return profileAllowsRelayFeature(config, "context") ? webToolsSteer(launchWebTools(config), true) : "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Always set, so a launch never inherits a parent agent's list. */
|
|
34
|
+
export function webToolsEnv(config: WebToolsConfig): Record<string, string> {
|
|
35
|
+
return writeChildEnv({}, { [WEB_TOOLS_ENV]: launchWebTools(config).join(","), AGENT_RELAY_WEB_SEARCH: webSearchEnabledFromEnv() ? "1" : "0" });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Codex hides switched-off tools on the flat bridge with its native per-server denylist. */
|
|
39
|
+
export function withDisabledWebTools(server: ProvisioningMcpServer, config: WebToolsConfig): ProvisioningMcpServer {
|
|
40
|
+
const disabled = disabledSharedWebTools(config);
|
|
41
|
+
return disabled.length ? { ...server, disabled_tools: disabled } : server;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** OpenCode names an MCP tool `<server>_<tool>`; its config `tools` map turns one off. */
|
|
45
|
+
export function openCodeWebToolsConfig(config: WebToolsConfig): { tools?: Record<string, false> } {
|
|
46
|
+
const disabled = disabledSharedWebTools(config);
|
|
47
|
+
return disabled.length ? { tools: Object.fromEntries(disabled.map((tool) => [`${SHARED_MCP_SERVER_NAME}_${tool}`, false])) } : {};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** #3250: Codex's one web_search tool also owns open/find; it cannot disable search and fetch separately. */
|
|
51
|
+
export function codexWebToolsConfigArgs(config: WebToolsConfig): string[] {
|
|
52
|
+
return launchWebTools(config).length ? ["-c", 'web_search="disabled"'] : [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** OpenCode 1.19.0: explicit permission rules follow the wildcard allow (tools=false is legacy). */
|
|
56
|
+
export function openCodeNativeWebPermissions(config: WebToolsConfig): Record<string, "deny"> {
|
|
57
|
+
const tools = launchWebTools(config);
|
|
58
|
+
return { ...(tools.includes("brave") ? { websearch: "deny" as const } : {}), ...(tools.includes("jina") ? { webfetch: "deny" as const } : {}) };
|
|
59
|
+
}
|
package/README.md
DELETED