@moondeck/station 0.0.1 → 1.0.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 +21 -0
- package/README.md +149 -2
- package/bin/uai-host.mjs +130 -0
- package/db/migrations/0000_host_tasks.sql +12 -0
- package/db/migrations/0001_host_ui.sql +11 -0
- package/db/migrations/0002_host_github_tokens.sql +8 -0
- package/db/migrations/0003_host_ssh_keys.sql +8 -0
- package/db/migrations/0004_host_owner_name.sql +1 -0
- package/db/migrations/0005_host_env_vars.sql +5 -0
- package/db/migrations/0006_host_project_env.sql +9 -0
- package/db/migrations/0007_host_github_token_kind.sql +7 -0
- package/db/migrations/0008_host_mcp_connections.sql +18 -0
- package/db/migrations/0009_host_agent_sessions.sql +15 -0
- package/db/migrations/0010_host_engine_accounts.sql +13 -0
- package/db/migrations/0011_codex_thread_id.sql +5 -0
- package/db/migrations/0012_agent_session_attach_key.sql +4 -0
- package/db/migrations/0013_github_credential_generations.sql +11 -0
- package/db/migrations/0014_host_inventory_event_index.sql +1 -0
- package/db/migrations/0015_host_settings.sql +9 -0
- package/db/migrations/0016_task_environment.sql +2 -0
- package/db/migrations/0017_engine_account_scope.sql +7 -0
- package/db/migrations/0018_engine_account_position.sql +3 -0
- package/db/migrations/0019_engine_account_store_ref.sql +3 -0
- package/db/migrations/0020_user_credential_store_ref.sql +6 -0
- package/db/migrations/0021_project_env_store_ref.sql +3 -0
- package/db/migrations/0022_task_project_ids.sql +5 -0
- package/db/migrations/0023_project_env_org.sql +4 -0
- package/db/migrations/0024_task_env_mode.sql +9 -0
- package/db/migrations/0025_project_env_authority.sql +147 -0
- package/db/migrations/0026_project_env_replica.sql +150 -0
- package/db/migrations/0027_cloud_credential_foundation.sql +51 -0
- package/db/migrations/0028_leased_engine_runtimes.sql +11 -0
- package/db/migrations/0029_task_shared_files_mode.sql +5 -0
- package/db/migrations/0030_credential_cleanup_restops.sql +12 -0
- package/db/migrations/0031_first_turn_receipts.sql +9 -0
- package/db/migrations/meta/_journal.json +230 -0
- package/db/schema.ts +871 -0
- package/images/standard/Dockerfile +347 -0
- package/images/standard/README.md +167 -0
- package/images/standard/container/code-server-settings.json +63 -0
- package/images/standard/container/corepack-version +1 -0
- package/images/standard/container/uai-init +704 -0
- package/images/standard/container/uai-materialize-runtimes +1527 -0
- package/images/standard/container/uai-task-forwarder.mjs +303 -0
- package/images/standard/tool-versions +2 -0
- package/lib/__fixtures__/skill-index/SKILL.md +10 -0
- package/lib/__fixtures__/skill-index/skills/accessibility-checklist.md +7 -0
- package/lib/__fixtures__/skill-index/skills/brand-voice.md +9 -0
- package/lib/__fixtures__/skill-index/skills/design-tokens.md +9 -0
- package/lib/agent-cli.ts +1507 -0
- package/lib/agent-errors.ts +22 -0
- package/lib/agent.ts +672 -0
- package/lib/agents/attach-compatibility.ts +51 -0
- package/lib/agents/claude.ts +918 -0
- package/lib/agents/codex-token-usage.fixture.json +130 -0
- package/lib/agents/codex.ts +1392 -0
- package/lib/agents/cursor-period-usage.ts +253 -0
- package/lib/agents/cursor.ts +562 -0
- package/lib/agents/dispatch.ts +78 -0
- package/lib/agents/durable-proc.ts +334 -0
- package/lib/agents/factory.ts +57 -0
- package/lib/agents/grok.ts +381 -0
- package/lib/agents/kimi.ts +329 -0
- package/lib/agents/machine-durable.ts +604 -0
- package/lib/agents/mock.ts +133 -0
- package/lib/agents/mode.ts +64 -0
- package/lib/agents/opencode.ts +508 -0
- package/lib/agents/output-source.ts +3 -0
- package/lib/agents/proc.ts +221 -0
- package/lib/agents/rate-limit.ts +337 -0
- package/lib/agents/registry.ts +182 -0
- package/lib/agents/session-state.ts +197 -0
- package/lib/agents/skill-index-preamble.fixture.ts +26 -0
- package/lib/agents/transport.ts +741 -0
- package/lib/agents/types.ts +254 -0
- package/lib/agents/uai.ts +106 -0
- package/lib/agents/usage.ts +44 -0
- package/lib/apple-one-off.ts +277 -0
- package/lib/apple-runtime-recycle.ts +237 -0
- package/lib/apple-uninstall-teardown.ts +224 -0
- package/lib/assignment-evidence.ts +94 -0
- package/lib/attachments.ts +48 -0
- package/lib/browser-testing.ts +1537 -0
- package/lib/capture-flow.ts +332 -0
- package/lib/cloud-credential-cleanup-adapter.ts +123 -0
- package/lib/cloud-credential-host.ts +512 -0
- package/lib/cloud-credential-leases.ts +2587 -0
- package/lib/cloud-credential-runtime.ts +1357 -0
- package/lib/cloud-state.ts +56 -0
- package/lib/codex-auth.ts +456 -0
- package/lib/command-db.ts +310 -0
- package/lib/compose-network-recovery.ts +283 -0
- package/lib/connection-secret-redaction.ts +25 -0
- package/lib/container-runtime.ts +1470 -0
- package/lib/credential-cleanup-inventory.ts +490 -0
- package/lib/credential-consumers/engine-anchor.ts +1880 -0
- package/lib/credential-consumers/engines.ts +797 -0
- package/lib/credential-consumers/files.ts +934 -0
- package/lib/credential-consumers/github.ts +702 -0
- package/lib/credential-consumers/mcp.ts +389 -0
- package/lib/credential-consumers/task-files.ts +127 -0
- package/lib/credential-consumers/task-secrets.ts +10 -0
- package/lib/credential-store-pickup.ts +149 -0
- package/lib/credential-store-scope.ts +112 -0
- package/lib/credential-store.ts +932 -0
- package/lib/credentials-activation-authority.ts +709 -0
- package/lib/credentials-purge.ts +1006 -0
- package/lib/custom-connection-capture.ts +395 -0
- package/lib/custom-connection-launch.ts +384 -0
- package/lib/custom-connection-runtime.ts +141 -0
- package/lib/db.ts +80 -0
- package/lib/disk-pressure.ts +42 -0
- package/lib/docker-exec.ts +156 -0
- package/lib/engine-accounts.ts +1970 -0
- package/lib/engine-connect.ts +321 -0
- package/lib/engine-credential-env.ts +52 -0
- package/lib/engine-login.ts +2933 -0
- package/lib/engines.ts +1342 -0
- package/lib/enrollment-state.ts +251 -0
- package/lib/env-file.ts +246 -0
- package/lib/env-schema.ts +937 -0
- package/lib/env.ts +195 -0
- package/lib/first-turn-receipts.ts +62 -0
- package/lib/fs-aliases.ts +49 -0
- package/lib/git-clone-credentials.ts +334 -0
- package/lib/git-diff.ts +530 -0
- package/lib/git-identity.ts +715 -0
- package/lib/github-git-auth.ts +207 -0
- package/lib/github-origin.ts +28 -0
- package/lib/github-tokens.ts +2439 -0
- package/lib/host-cloud-url.ts +62 -0
- package/lib/host-config.ts +279 -0
- package/lib/host-env.ts +424 -0
- package/lib/host-logs.ts +965 -0
- package/lib/internal-connections.ts +5 -0
- package/lib/keyed-promise-tail.ts +23 -0
- package/lib/launch-mode.ts +25 -0
- package/lib/lease.ts +239 -0
- package/lib/legacy-host.ts +769 -0
- package/lib/legacy-runtime-retirement.ts +424 -0
- package/lib/legacy-runtime-v1.fixture.ts +627 -0
- package/lib/machine-exec.ts +238 -0
- package/lib/machine-gateway-tunnel.ts +344 -0
- package/lib/machine-keys.ts +65 -0
- package/lib/machine-project-env-loader.ts +152 -0
- package/lib/machine-provider-aws.ts +923 -0
- package/lib/machine-provider-local.ts +338 -0
- package/lib/machine-provider.ts +90 -0
- package/lib/managed-activation-watcher.ts +72 -0
- package/lib/managed-install-owner-watcher.ts +55 -0
- package/lib/managed-operation-drain.ts +57 -0
- package/lib/managed-runtime.ts +4296 -0
- package/lib/managed-update-scheduler.ts +125 -0
- package/lib/mcp-capture.ts +865 -0
- package/lib/mcp-config-lock.ts +2 -0
- package/lib/mcp-connections.ts +959 -0
- package/lib/mcp-gateway.ts +2594 -0
- package/lib/obs.ts +512 -0
- package/lib/openrouter-budget.ts +247 -0
- package/lib/orchestrator.ts +11409 -0
- package/lib/person-capture.ts +327 -0
- package/lib/preview-ports.ts +85 -0
- package/lib/preview-sidecar.ts +331 -0
- package/lib/project-env-authority.ts +1319 -0
- package/lib/project-env-feature.ts +74 -0
- package/lib/project-env-legacy-tags.ts +130 -0
- package/lib/project-env-loader.ts +242 -0
- package/lib/project-env-replica.ts +2094 -0
- package/lib/project-env-schema-capture.ts +342 -0
- package/lib/project-env-staging.ts +148 -0
- package/lib/project-env-task-policy.ts +176 -0
- package/lib/project-env-task-replica.ts +1752 -0
- package/lib/release-feed.ts +61 -0
- package/lib/release-manifest.ts +710 -0
- package/lib/release-trust.ts +46 -0
- package/lib/repo-clone.ts +38 -0
- package/lib/routing/shadow.ts +261 -0
- package/lib/runner-boot.ts +10 -0
- package/lib/runtime-activation-tail.ts +279 -0
- package/lib/runtime-archive.ts +1216 -0
- package/lib/runtime-authority.ts +166 -0
- package/lib/runtime-guard.ts +36 -0
- package/lib/runtime-provider-state.ts +169 -0
- package/lib/runtime-reconnect.ts +86 -0
- package/lib/runtime-state.ts +1033 -0
- package/lib/safe-http.ts +364 -0
- package/lib/secrets-proxy/address.ts +95 -0
- package/lib/secrets-proxy/audit.ts +30 -0
- package/lib/secrets-proxy/ca-store.ts +200 -0
- package/lib/secrets-proxy/ca.ts +470 -0
- package/lib/secrets-proxy/container-steps.ts +138 -0
- package/lib/secrets-proxy/der.ts +273 -0
- package/lib/secrets-proxy/engine.ts +1633 -0
- package/lib/secrets-proxy/host.ts +93 -0
- package/lib/secrets-proxy/hostname.ts +57 -0
- package/lib/secrets-proxy/placeholders.ts +196 -0
- package/lib/secrets-proxy/policy.ts +150 -0
- package/lib/secrets-proxy/readiness.ts +148 -0
- package/lib/secrets-proxy/runtime.ts +582 -0
- package/lib/secrets-proxy/scrub.ts +559 -0
- package/lib/secrets-proxy/sidecar-view.ts +139 -0
- package/lib/secrets-proxy/task-scripts.ts +262 -0
- package/lib/secrets-proxy/task-trust.ts +605 -0
- package/lib/secrets-proxy/trust-watch.ts +80 -0
- package/lib/secrets.ts +71 -0
- package/lib/shared-files-s3.ts +976 -0
- package/lib/shared-files.ts +156 -0
- package/lib/sidecar-assign.ts +49 -0
- package/lib/sidecar-guard.ts +58 -0
- package/lib/sidecar-resume.ts +178 -0
- package/lib/sidecar.ts +69 -0
- package/lib/signin-box-credentials.ts +465 -0
- package/lib/signin-box.ts +114 -0
- package/lib/skill-index-example.fixture.ts +56 -0
- package/lib/skill-layout.ts +417 -0
- package/lib/skills.ts +489 -0
- package/lib/ssh.ts +401 -0
- package/lib/standard-image-cleanup.ts +117 -0
- package/lib/standard-image-pin.ts +207 -0
- package/lib/standard-image.ts +1697 -0
- package/lib/stopped-task-status-queue.ts +120 -0
- package/lib/task-container-cli.ts +269 -0
- package/lib/task-credential-cleanup.ts +752 -0
- package/lib/task-diff-sandbox.ts +464 -0
- package/lib/task-diff.ts +91 -0
- package/lib/task-env-forwarder.ts +228 -0
- package/lib/task-env-source.ts +249 -0
- package/lib/task-environment/anchor.ts +31 -0
- package/lib/task-environment/apple-container.ts +860 -0
- package/lib/task-environment/docker.ts +1344 -0
- package/lib/task-environment/index.ts +466 -0
- package/lib/task-environment/legacy-adoption.ts +467 -0
- package/lib/task-environment/machine-environment.ts +61 -0
- package/lib/task-environment/machine-task-up.ts +1227 -0
- package/lib/task-environment/machine.ts +974 -0
- package/lib/task-environment/registry.ts +59 -0
- package/lib/task-environment/self-identity.ts +20 -0
- package/lib/task-environment/self.ts +566 -0
- package/lib/task-environment/types.ts +642 -0
- package/lib/task-environment/workspace-files.ts +685 -0
- package/lib/task-environment/workspace-runtime.fixture.ts +50 -0
- package/lib/task-files-machine-retirement-outbox.ts +592 -0
- package/lib/task-files-machine-retirement.ts +116 -0
- package/lib/task-files-readiness.ts +127 -0
- package/lib/task-gateway-port.ts +22 -0
- package/lib/task-gateway-protocol.ts +190 -0
- package/lib/task-gateway-recovery.ts +47 -0
- package/lib/task-gateway-state.ts +512 -0
- package/lib/task-identity.ts +19 -0
- package/lib/task-inventory.ts +682 -0
- package/lib/task-machine-credentials-activation.ts +95 -0
- package/lib/task-machine-swap-safety.ts +729 -0
- package/lib/task-person-credentials.ts +76 -0
- package/lib/task-project-ops-host.ts +147 -0
- package/lib/task-project-ops.ts +1062 -0
- package/lib/task-secret-staging.ts +16 -0
- package/lib/task-status.ts +46 -0
- package/lib/task-token.ts +46 -0
- package/lib/token-broker-adopt.ts +344 -0
- package/lib/token-broker-lambda.ts +105 -0
- package/lib/token-broker-local.ts +392 -0
- package/lib/token-broker-revision.ts +310 -0
- package/lib/token-broker-ssm.ts +525 -0
- package/lib/token-broker.ts +218 -0
- package/lib/token-lanes/openai-oauth.ts +123 -0
- package/lib/token-lanes/registry.ts +45 -0
- package/lib/token-lanes/types.ts +163 -0
- package/lib/token-lanes/xai-oauth.ts +165 -0
- package/lib/transcript.ts +128 -0
- package/lib/tunnel-registry.ts +220 -0
- package/lib/tunnel-runtime.ts +235 -0
- package/lib/ulid.ts +16 -0
- package/lib/user-bus-env.ts +116 -0
- package/lib/user-credentials-store.ts +435 -0
- package/lib/vnode-pressure.ts +142 -0
- package/package.json +115 -8
- package/runner/runner.mjs +442 -0
- package/scripts/agent/_common.sh +633 -0
- package/scripts/agent/env-hook.sh +626 -0
- package/scripts/agent/moondeck-cli.sh +5 -0
- package/scripts/agent/task-down.sh +291 -0
- package/scripts/agent/task-status.sh +70 -0
- package/scripts/agent/task-up.sh +3761 -0
- package/scripts/agent/uai-env-exec.sh +26 -0
- package/scripts/install/darwin.ts +1048 -0
- package/scripts/install/linux.ts +958 -0
- package/scripts/install/types.ts +78 -0
- package/scripts/install/util.ts +254 -0
- package/scripts/install/win.ts +143 -0
- package/src/activation-respawn.ts +155 -0
- package/src/apple-tunnel-route.ts +104 -0
- package/src/bridge-diagnostics.ts +39 -0
- package/src/capture-wire.ts +102 -0
- package/src/cli.ts +2585 -0
- package/src/cloud-credentials.ts +2144 -0
- package/src/cloud-origins.ts +23 -0
- package/src/credential-location.ts +248 -0
- package/src/credential-wire-validation.ts +25 -0
- package/src/custom-connections.ts +237 -0
- package/src/engine-account-paths.ts +11 -0
- package/src/engine-models.ts +39 -0
- package/src/engine-usage.ts +104 -0
- package/src/enrollment-next-steps.ts +24 -0
- package/src/event-outbox.ts +185 -0
- package/src/index.ts +2907 -0
- package/src/load-env.ts +97 -0
- package/src/machine-tunnel-route.ts +156 -0
- package/src/main.ts +6363 -0
- package/src/never-proxied-domains.ts +31 -0
- package/src/outbound-frame.ts +31 -0
- package/src/paths.ts +81 -0
- package/src/project-env-latest.ts +130 -0
- package/src/project-env-policy.ts +396 -0
- package/src/project-env-wording.ts +53 -0
- package/src/protocol.ts +5550 -0
- package/src/runtime-bootstrap.ts +165 -0
- package/src/station-identity.ts +96 -0
- package/src/task-env-names.ts +28 -0
- package/src/task-secrets.ts +75 -0
- package/src/tunnel-dial.ts +102 -0
- package/src/ui/server.ts +852 -0
- package/src/ui/types.ts +173 -0
- package/src/workspace-path-policy.ts +41 -0
- package/ui/app.js +1102 -0
- package/ui/index.html +90 -0
- package/ui/style.css +685 -0
- package/ui/uai-favicon.svg +21 -0
- package/ui/uai-logo-black.svg +9 -0
- package/ui/uai-wheel.svg +9 -0
- package/bin/moondeck-station.mjs +0 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moondeck/station",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Moondeck Station: runs Moondeck tasks on a machine you control.
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Moondeck Station: runs Moondeck tasks in containers on a machine you control.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Moondeck <team@moondeck.io>",
|
|
7
7
|
"homepage": "https://moondeck.io",
|
|
@@ -10,17 +10,124 @@
|
|
|
10
10
|
"url": "git+https://github.com/moondeckio/uai.git",
|
|
11
11
|
"directory": "host-agent"
|
|
12
12
|
},
|
|
13
|
-
"
|
|
14
|
-
"
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/moondeckio/uai/issues"
|
|
15
15
|
},
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ai",
|
|
18
|
+
"coding-agent",
|
|
19
|
+
"claude",
|
|
20
|
+
"codex",
|
|
21
|
+
"docker",
|
|
22
|
+
"dev-container",
|
|
23
|
+
"self-hosted",
|
|
24
|
+
"orchestrator"
|
|
19
25
|
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"bin": {
|
|
28
|
+
"moondeck-station": "bin/uai-host.mjs"
|
|
29
|
+
},
|
|
20
30
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
31
|
+
"node": "22.x || >=24 <27"
|
|
22
32
|
},
|
|
23
33
|
"publishConfig": {
|
|
24
34
|
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"bin",
|
|
38
|
+
"src",
|
|
39
|
+
"lib",
|
|
40
|
+
"db",
|
|
41
|
+
"scripts/agent",
|
|
42
|
+
"scripts/install",
|
|
43
|
+
"runner",
|
|
44
|
+
"images/standard",
|
|
45
|
+
"ui",
|
|
46
|
+
"README.md",
|
|
47
|
+
"!**/*.test.ts",
|
|
48
|
+
"!**/*.bats",
|
|
49
|
+
"!scripts/agent/tests"
|
|
50
|
+
],
|
|
51
|
+
"exports": {
|
|
52
|
+
".": {
|
|
53
|
+
"types": "./src/index.ts",
|
|
54
|
+
"default": "./src/index.ts"
|
|
55
|
+
},
|
|
56
|
+
"./protocol": {
|
|
57
|
+
"types": "./src/protocol.ts",
|
|
58
|
+
"default": "./src/protocol.ts"
|
|
59
|
+
},
|
|
60
|
+
"./cloud-credentials": {
|
|
61
|
+
"types": "./src/cloud-credentials.ts",
|
|
62
|
+
"default": "./src/cloud-credentials.ts"
|
|
63
|
+
},
|
|
64
|
+
"./project-env-policy": {
|
|
65
|
+
"types": "./src/project-env-policy.ts",
|
|
66
|
+
"default": "./src/project-env-policy.ts"
|
|
67
|
+
},
|
|
68
|
+
"./project-env-latest": {
|
|
69
|
+
"types": "./src/project-env-latest.ts",
|
|
70
|
+
"default": "./src/project-env-latest.ts"
|
|
71
|
+
},
|
|
72
|
+
"./project-env-wording": {
|
|
73
|
+
"types": "./src/project-env-wording.ts",
|
|
74
|
+
"default": "./src/project-env-wording.ts"
|
|
75
|
+
},
|
|
76
|
+
"./engine-models": {
|
|
77
|
+
"types": "./src/engine-models.ts",
|
|
78
|
+
"default": "./src/engine-models.ts"
|
|
79
|
+
},
|
|
80
|
+
"./engine-usage": {
|
|
81
|
+
"types": "./src/engine-usage.ts",
|
|
82
|
+
"default": "./src/engine-usage.ts"
|
|
83
|
+
},
|
|
84
|
+
"./workspace-path-policy": {
|
|
85
|
+
"types": "./src/workspace-path-policy.ts",
|
|
86
|
+
"default": "./src/workspace-path-policy.ts"
|
|
87
|
+
},
|
|
88
|
+
"./task-secrets": {
|
|
89
|
+
"types": "./src/task-secrets.ts",
|
|
90
|
+
"default": "./src/task-secrets.ts"
|
|
91
|
+
},
|
|
92
|
+
"./task-env-names": {
|
|
93
|
+
"types": "./src/task-env-names.ts",
|
|
94
|
+
"default": "./src/task-env-names.ts"
|
|
95
|
+
},
|
|
96
|
+
"./cloud-origins": {
|
|
97
|
+
"types": "./src/cloud-origins.ts",
|
|
98
|
+
"default": "./src/cloud-origins.ts"
|
|
99
|
+
},
|
|
100
|
+
"./never-proxied-domains": {
|
|
101
|
+
"types": "./src/never-proxied-domains.ts",
|
|
102
|
+
"default": "./src/never-proxied-domains.ts"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"scripts": {
|
|
106
|
+
"lint": "eslint .",
|
|
107
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
108
|
+
"test:unit": "vitest run --config vitest.config.ts",
|
|
109
|
+
"start": "tsx src/cli.ts run",
|
|
110
|
+
"moondeck-station": "tsx src/cli.ts"
|
|
111
|
+
},
|
|
112
|
+
"dependencies": {
|
|
113
|
+
"@aws-sdk/client-ssm": "3.899.0",
|
|
114
|
+
"@sentry/node": "^10.66.0",
|
|
115
|
+
"better-sqlite3": "12.11.1",
|
|
116
|
+
"dotenv": "^16.4.5",
|
|
117
|
+
"drizzle-orm": "^0.36.0",
|
|
118
|
+
"tsx": "^4.19.2",
|
|
119
|
+
"ulid": "^2.3.0",
|
|
120
|
+
"ws": "^8.18.3",
|
|
121
|
+
"zod": "^3.23.8"
|
|
122
|
+
},
|
|
123
|
+
"devDependencies": {
|
|
124
|
+
"@types/node": "^22.9.0",
|
|
125
|
+
"@types/ws": "^8.18.1",
|
|
126
|
+
"@typescript-eslint/eslint-plugin": "^8.59.4",
|
|
127
|
+
"@typescript-eslint/parser": "^8.59.4",
|
|
128
|
+
"esbuild": "0.25.8",
|
|
129
|
+
"eslint": "^9.14.0",
|
|
130
|
+
"typescript": "^5.6.3",
|
|
131
|
+
"vitest": "^2.1.4"
|
|
25
132
|
}
|
|
26
133
|
}
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* uai session runner (ADR-061) — runs INSIDE the task container and owns an
|
|
4
|
+
* agent CLI process, so the CLI's lifetime is decoupled from the host
|
|
5
|
+
* process. The host talks to it only through files on the task-workspace
|
|
6
|
+
* bind mount (no sockets/FIFOs — they don't cross the macOS↔VM boundary):
|
|
7
|
+
*
|
|
8
|
+
* <sessionDir>/inbox.jsonl host appends; runner tails → CLI stdin
|
|
9
|
+
* <sessionDir>/outbox.jsonl CLI stdout protocol + __uai meta (connections redacted)
|
|
10
|
+
* <sessionDir>/heartbeat rewritten every HEARTBEAT_MS (host checks mtime)
|
|
11
|
+
* <sessionDir>/runner.json pid, protocol, argv, startedAt
|
|
12
|
+
*
|
|
13
|
+
* Meta lines are `{"__uai":"spawn"|"exit", ...}`; the host filters them out
|
|
14
|
+
* before handing lines to the protocol adapters. Host metadata/control lines
|
|
15
|
+
* in the inbox use the same shape (`input` timestamps and `stop`); the runner
|
|
16
|
+
* filters those, while every protocol frame goes to the CLI's stdin untouched.
|
|
17
|
+
*
|
|
18
|
+
* Plain Node ≥18, dependency-free, ESM. Testable outside docker: point it
|
|
19
|
+
* at a tmp dir and any line-oriented fake CLI.
|
|
20
|
+
*
|
|
21
|
+
* Usage: node runner.mjs <sessionDir> -- <command> [args...]
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawn } from "node:child_process";
|
|
25
|
+
import { appendFileSync, mkdirSync, openSync, readSync, fstatSync, closeSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import process from "node:process";
|
|
28
|
+
|
|
29
|
+
const PROTOCOL = 1;
|
|
30
|
+
const POLL_MS = 50;
|
|
31
|
+
const HEARTBEAT_MS = 5_000;
|
|
32
|
+
const STDERR_CAP = 8 * 1024;
|
|
33
|
+
const STOP_GRACE_MS = 5_000;
|
|
34
|
+
const customConnection = process.env.UAI_CUSTOM_CONNECTION === "1";
|
|
35
|
+
const connectionKeys = [...new Set([
|
|
36
|
+
process.env.ANTHROPIC_AUTH_TOKEN,
|
|
37
|
+
process.env.ANTHROPIC_API_KEY,
|
|
38
|
+
process.env.UAI_CUSTOM_API_KEY,
|
|
39
|
+
process.env.AWS_BEARER_TOKEN_BEDROCK,
|
|
40
|
+
].filter((key) => typeof key === "string" && key.length > 0))]
|
|
41
|
+
.sort((a, b) => b.length - a.length);
|
|
42
|
+
|
|
43
|
+
function redactConnectionKeys(text) {
|
|
44
|
+
for (const key of connectionKeys) {
|
|
45
|
+
text = text.split(key).join("[redacted]".includes(key) ? "" : "[redacted]");
|
|
46
|
+
}
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Never persist upstream diagnostics from a custom endpoint: these can contain
|
|
51
|
+
// credentials, request bodies or arbitrary provider responses, beyond known keys.
|
|
52
|
+
function connectionReason(value) {
|
|
53
|
+
const text = typeof value === "string" ? value : JSON.stringify(value) ?? "";
|
|
54
|
+
if (/401|unauthorized|authentication_failed|unrecognized.?client|expired.?token|token.{0,80}(?:expired|revoked)|invalid.?api.?key/i.test(text)) return "unauthorized";
|
|
55
|
+
if (/403|forbidden|access.?denied/i.test(text)) return "forbidden";
|
|
56
|
+
if (/404|model.{0,30}not.found/i.test(text)) return "model-not-found";
|
|
57
|
+
if (/429|rate.?limit|throttl/i.test(text)) return "rate-limited";
|
|
58
|
+
if (/expired/i.test(text)) return "expired";
|
|
59
|
+
if (/408|timeout|timed.out/i.test(text)) return "timeout";
|
|
60
|
+
return "upstream-error";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function connectionError(value) {
|
|
64
|
+
const reason = connectionReason(value);
|
|
65
|
+
// JSON-RPC error responses must retain their error object and numeric code.
|
|
66
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
67
|
+
? { ...(typeof value.code === "number" ? { code: value.code } : {}), message: reason }
|
|
68
|
+
: reason;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Tool/model payloads can contain fields named error, result or stderrTail.
|
|
72
|
+
// Preserve their meaning; only exact credentials are redacted recursively.
|
|
73
|
+
function redactConnectionValue(value) {
|
|
74
|
+
if (typeof value === "string") return redactConnectionKeys(value);
|
|
75
|
+
if (Array.isArray(value)) return value.map(redactConnectionValue);
|
|
76
|
+
if (!value || typeof value !== "object") return value;
|
|
77
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) =>
|
|
78
|
+
[redactConnectionKeys(key), redactConnectionValue(item)]));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Suppress provider diagnostics only in the CLI's protocol-level failures,
|
|
82
|
+
// never an error object nested in a completed tool item or model output.
|
|
83
|
+
function redactConnectionRecord(value) {
|
|
84
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return redactConnectionValue(value);
|
|
85
|
+
const claudeApiError = value.type === "assistant" && value.is_api_error_message === true;
|
|
86
|
+
const claudeResultError = value.type === "result" && value.is_error === true;
|
|
87
|
+
const protocolError = claudeApiError || claudeResultError ||
|
|
88
|
+
(value.type === "system" && value.subtype === "api_retry") ||
|
|
89
|
+
value.type === "error" || value.type === "turn.failed" ||
|
|
90
|
+
(Object.hasOwn(value, "id") && value.error);
|
|
91
|
+
if (claudeApiError) {
|
|
92
|
+
value = { ...value, message: { ...value.message,
|
|
93
|
+
content: [{ type: "text", text: connectionReason(value) }] } };
|
|
94
|
+
}
|
|
95
|
+
if (protocolError) {
|
|
96
|
+
value = { ...value };
|
|
97
|
+
if (Object.hasOwn(value, "error")) value.error = connectionError(value.error);
|
|
98
|
+
if (Array.isArray(value.errors)) value.errors = value.errors.map(connectionError);
|
|
99
|
+
if (claudeResultError && Object.hasOwn(value, "result")) value.result = connectionReason(value.result);
|
|
100
|
+
if (value.type === "error") {
|
|
101
|
+
for (const key of ["message", "detail", "body"]) {
|
|
102
|
+
if (Object.hasOwn(value, key)) value[key] = connectionReason(value[key]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (value.method === "error" && value.params?.error) {
|
|
107
|
+
value = { ...value, params: { ...value.params, error: connectionError(value.params.error) } };
|
|
108
|
+
}
|
|
109
|
+
if (value.method === "turn/completed" && value.params?.turn?.status === "failed" && value.params.turn.error) {
|
|
110
|
+
value = { ...value, params: { ...value.params,
|
|
111
|
+
turn: { ...value.params.turn, error: connectionError(value.params.turn.error) } } };
|
|
112
|
+
}
|
|
113
|
+
if (value.__uai === "exit" && Object.hasOwn(value, "stderrTail")) {
|
|
114
|
+
value = { ...value, stderrTail: value.stderrTail ? connectionReason(value.stderrTail) : "" };
|
|
115
|
+
}
|
|
116
|
+
return redactConnectionValue(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function safeOutboxLine(line) {
|
|
120
|
+
if (!customConnection) return line;
|
|
121
|
+
// Wait for a complete JSON line, decode its strings before redacting, then
|
|
122
|
+
// serialize again. Replacing raw JSON bytes would miss escaped keys or corrupt
|
|
123
|
+
// quote/backslash-containing keys. Separate protocol deltas retain their shape;
|
|
124
|
+
// the host also redacts across assembled message deltas before publication.
|
|
125
|
+
try {
|
|
126
|
+
return JSON.stringify(redactConnectionRecord(JSON.parse(line)));
|
|
127
|
+
} catch {
|
|
128
|
+
return JSON.stringify({ type: "error", error: connectionReason(line) });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// SDK/app-server retries must not hide a rejected connection credential.
|
|
133
|
+
// Restrict classification to CLI error records, never generated model text.
|
|
134
|
+
let connectionFailureSeen = false;
|
|
135
|
+
let connectionTransientRetries = 0;
|
|
136
|
+
// Codex's built-in AWS backend has a fixed HTTP retry budget and rejects
|
|
137
|
+
// retry overrides. Do not multiply that budget with another stream cycle.
|
|
138
|
+
const connectionStreamRetryBudget = process.env.AWS_BEARER_TOKEN_BEDROCK &&
|
|
139
|
+
process.env.CLAUDE_CODE_USE_BEDROCK !== "1" ? 0 : 1;
|
|
140
|
+
function connectionFailureFrame(value) {
|
|
141
|
+
if (!customConnection) return value;
|
|
142
|
+
if (value.method === "turn/started" || value.type === "turn.started" ||
|
|
143
|
+
(value.type === "result" && value.is_error === false)) connectionTransientRetries = 0;
|
|
144
|
+
const claudeRetry = value.type === "system" && value.subtype === "api_retry";
|
|
145
|
+
const rpcError = value.error && Object.hasOwn(value, "id");
|
|
146
|
+
const failed = claudeRetry || rpcError || value.type === "error" || value.type === "turn.failed" ||
|
|
147
|
+
(value.type === "assistant" && value.is_api_error_message === true) ||
|
|
148
|
+
(value.type === "result" && value.is_error === true) || value.method === "error" ||
|
|
149
|
+
(value.method === "turn/completed" && value.params?.turn?.status === "failed");
|
|
150
|
+
if (!failed) return value;
|
|
151
|
+
const detail = value.params?.error ?? value.params?.turn?.error ?? value.error ?? value.message ?? value.result ?? "";
|
|
152
|
+
const text = typeof detail === "string" ? detail : JSON.stringify(detail);
|
|
153
|
+
const status = value.error_status ?? detail?.status ?? detail?.statusCode ??
|
|
154
|
+
([401, 403].includes(detail?.code) ? detail.code : undefined);
|
|
155
|
+
const auth = status === 401 || status === 403 ||
|
|
156
|
+
/\b(?:status|code|API Error:)\s*[:=]?\s*(?:401|403)\b|\b401 Unauthorized\b|\b403 Forbidden\b|authentication_failed|unrecognized.?client|access.?denied|expired.?token|invalid.?api.?key|invalid.?bearer.?token|security token.{0,80}(?:expired|invalid)/i.test(text);
|
|
157
|
+
const retry = claudeRetry || value.params?.willRetry === true ||
|
|
158
|
+
(value.type === "error" && /^Reconnecting\.\.\./.test(text));
|
|
159
|
+
if (!auth && (!retry || ++connectionTransientRetries <= connectionStreamRetryBudget)) return value;
|
|
160
|
+
const reason = auth ? (status === 403 && !/authentication_failed/i.test(text) ? "forbidden" : connectionReason(text)) : connectionReason(value);
|
|
161
|
+
const safeReason = auth && !["unauthorized", "forbidden", "expired"].includes(reason) ? "unauthorized" : reason;
|
|
162
|
+
connectionFailureSeen = true;
|
|
163
|
+
stopCli();
|
|
164
|
+
if (rpcError) return { jsonrpc: value.jsonrpc, id: value.id, error: { ...(typeof value.error.code === "number" ? { code: value.error.code } : {}), message: safeReason } };
|
|
165
|
+
if (value.method) return { method: "error", params: { threadId: value.params?.threadId,
|
|
166
|
+
turnId: value.params?.turnId, willRetry: false, error: { message: safeReason } } };
|
|
167
|
+
if (["system", "assistant", "result"].includes(value.type)) return {
|
|
168
|
+
type: "result", subtype: "error_during_execution", is_error: true,
|
|
169
|
+
result: safeReason, errors: [safeReason],
|
|
170
|
+
};
|
|
171
|
+
return { type: "error", message: safeReason };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Retain only a possible credential prefix across protocol deltas. Every
|
|
175
|
+
// persisted text fragment is safe when concatenated with earlier fragments.
|
|
176
|
+
const deltaTails = new Map();
|
|
177
|
+
let deltaTailBytes = 0;
|
|
178
|
+
// Bound retained bytes, rather than stopping a healthy multi-item turn at
|
|
179
|
+
// an arbitrary item count. Completed items release their state immediately.
|
|
180
|
+
const DELTA_TAIL_MAX_BYTES = 4 * 1024 * 1024;
|
|
181
|
+
function forgetDeltaTail(stream) {
|
|
182
|
+
const pending = deltaTails.get(stream);
|
|
183
|
+
if (pending) deltaTailBytes -= pending.bytes;
|
|
184
|
+
deltaTails.delete(stream);
|
|
185
|
+
}
|
|
186
|
+
function streamingDelta(value) {
|
|
187
|
+
if (value.type === "stream_event" && value.event?.type === "content_block_delta") {
|
|
188
|
+
for (const field of ["text", "thinking", "partial_json", "signature"]) {
|
|
189
|
+
if (typeof value.event.delta?.[field] === "string") {
|
|
190
|
+
return { stream: `claude:${value.event.index ?? 0}:${field}`, target: value.event.delta, field };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (typeof value.method === "string" && /delta$/i.test(value.method) && typeof value.params?.delta === "string") {
|
|
195
|
+
return { stream: `codex:${value.method}:${value.params.threadId ?? ""}:${value.params.turnId ?? ""}:${value.params.itemId ?? ""}`,
|
|
196
|
+
target: value.params, field: "delta" };
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
function safeOutboxLines(line) {
|
|
201
|
+
if (!customConnection) return [line];
|
|
202
|
+
let value;
|
|
203
|
+
try { value = JSON.parse(line); } catch { return [safeOutboxLine(line)]; }
|
|
204
|
+
if (!value || typeof value !== "object") return [safeOutboxLine(line)];
|
|
205
|
+
if (connectionFailureSeen && value.__uai !== "exit") return [];
|
|
206
|
+
value = connectionFailureFrame(value);
|
|
207
|
+
const delta = streamingDelta(value);
|
|
208
|
+
if (delta) {
|
|
209
|
+
const text = redactConnectionKeys((deltaTails.get(delta.stream)?.tail ?? "") + delta.target[delta.field]);
|
|
210
|
+
let keep = 0;
|
|
211
|
+
for (const key of connectionKeys) {
|
|
212
|
+
for (let n = Math.min(key.length - 1, text.length); n > keep; n--) {
|
|
213
|
+
if (text.endsWith(key.slice(0, n))) { keep = n; break; }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const tail = keep ? text.slice(-keep) : "";
|
|
217
|
+
delta.target[delta.field] = keep ? text.slice(0, -keep) : text;
|
|
218
|
+
forgetDeltaTail(delta.stream);
|
|
219
|
+
if (tail) {
|
|
220
|
+
// Only retain the protocol envelope, not a potentially large provider
|
|
221
|
+
// payload or the already-persisted text.
|
|
222
|
+
const frame = value.type === "stream_event"
|
|
223
|
+
? { type: value.type, event: { type: value.event.type, index: value.event.index,
|
|
224
|
+
delta: { type: value.event.delta.type, [delta.field]: "" } } }
|
|
225
|
+
: { method: value.method, params: { threadId: value.params.threadId,
|
|
226
|
+
turnId: value.params.turnId, itemId: value.params.itemId, delta: "" } };
|
|
227
|
+
const bytes = Buffer.byteLength(delta.stream) + Buffer.byteLength(tail) + Buffer.byteLength(JSON.stringify(frame));
|
|
228
|
+
if (deltaTailBytes + bytes > DELTA_TAIL_MAX_BYTES) {
|
|
229
|
+
stopCli();
|
|
230
|
+
return [JSON.stringify({ type: "error", error: "upstream-error" })];
|
|
231
|
+
}
|
|
232
|
+
deltaTails.set(delta.stream, { tail, frame, bytes });
|
|
233
|
+
deltaTailBytes += bytes;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const lines = [];
|
|
237
|
+
for (const [stream, pending] of deltaTails) {
|
|
238
|
+
const threadMatches = value.params?.threadId === undefined || pending.frame.params?.threadId === value.params.threadId;
|
|
239
|
+
const completedTurn = value.params?.turnId ?? value.params?.turn?.id;
|
|
240
|
+
const turnMatches = completedTurn === undefined || pending.frame.params?.turnId === completedTurn;
|
|
241
|
+
const terminal = value.__uai === "exit" ||
|
|
242
|
+
(value.type === "result" && pending.frame.type === "stream_event") ||
|
|
243
|
+
(value.method === "turn/completed" && pending.frame.params && threadMatches && turnMatches);
|
|
244
|
+
const claudeEnd = value.type === "stream_event" && value.event?.type === "content_block_stop" &&
|
|
245
|
+
pending.frame.type === "stream_event" && pending.frame.event.index === value.event.index;
|
|
246
|
+
const codexEnd = value.method === "item/completed" && pending.frame.params &&
|
|
247
|
+
typeof (value.params?.item?.id ?? value.params?.itemId) === "string" &&
|
|
248
|
+
pending.frame.params.itemId === (value.params.item?.id ?? value.params.itemId) &&
|
|
249
|
+
(value.params.threadId === undefined || pending.frame.params.threadId === value.params.threadId) &&
|
|
250
|
+
(value.params.turnId === undefined || pending.frame.params.turnId === value.params.turnId);
|
|
251
|
+
if (!terminal && !claudeEnd && !codexEnd) continue;
|
|
252
|
+
const d = streamingDelta(pending.frame);
|
|
253
|
+
d.target[d.field] = pending.tail;
|
|
254
|
+
lines.push(JSON.stringify(redactConnectionRecord(pending.frame)));
|
|
255
|
+
forgetDeltaTail(stream);
|
|
256
|
+
}
|
|
257
|
+
lines.push(JSON.stringify(redactConnectionRecord(value)));
|
|
258
|
+
return lines;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---- argv ------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
const sep = process.argv.indexOf("--");
|
|
264
|
+
const sessionDir = process.argv[2];
|
|
265
|
+
if (!sessionDir || sep < 0 || sep + 1 >= process.argv.length) {
|
|
266
|
+
process.stderr.write("usage: runner.mjs <sessionDir> -- <command> [args...]\n");
|
|
267
|
+
process.exit(2);
|
|
268
|
+
}
|
|
269
|
+
const command = process.argv[sep + 1];
|
|
270
|
+
const args = process.argv.slice(sep + 2);
|
|
271
|
+
|
|
272
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
273
|
+
const inboxPath = join(sessionDir, "inbox.jsonl");
|
|
274
|
+
const outboxPath = join(sessionDir, "outbox.jsonl");
|
|
275
|
+
const heartbeatPath = join(sessionDir, "heartbeat");
|
|
276
|
+
const runnerJsonPath = join(sessionDir, "runner.json");
|
|
277
|
+
|
|
278
|
+
// Single append point — appendFileSync with O_APPEND keeps lines atomic for
|
|
279
|
+
// the sizes we write; the runner is the outbox's only writer.
|
|
280
|
+
function outbox(line) {
|
|
281
|
+
for (const safe of safeOutboxLines(line)) {
|
|
282
|
+
appendFileSync(outboxPath, safe.endsWith("\n") ? safe : `${safe}\n`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function meta(kind, extra = {}) {
|
|
286
|
+
outbox(JSON.stringify({ __uai: kind, ts: Date.now(), ...extra }));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---- the CLI ---------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
// The runner is launched through the container's asdf `node` shim, which
|
|
292
|
+
// exports its resolved version (ASDF_NODEJS_VERSION et al.) into our env.
|
|
293
|
+
// Passing that through would pin the CLI's own asdf shim to the WORKSPACE's
|
|
294
|
+
// node version — "No claude executable found for nodejs X" when the agent
|
|
295
|
+
// CLIs are installed under a different one. So strip the per-tool VERSION
|
|
296
|
+
// PINS only (ASDF_NODEJS_VERSION, ASDF_<TOOL>_VERSION) — NOT the whole ASDF_*
|
|
297
|
+
// namespace: ASDF_DATA_DIR (/opt/asdf-data) and ASDF_DIR (/opt/asdf) tell the
|
|
298
|
+
// shim's `asdf exec` WHERE the tools live. Drop those and asdf falls back to
|
|
299
|
+
// $HOME/.asdf (empty), so EVERY shim aborts with "unknown command: codex.
|
|
300
|
+
// Perhaps you have to reshim?" (exit 1) — the codex/claude resume failure on
|
|
301
|
+
// every host restart, host 0.8.32. `ASDF_VERSION` (asdf's own version) has no
|
|
302
|
+
// middle segment, so the pin regex leaves it — harmless either way.
|
|
303
|
+
const cliEnv = { ...process.env };
|
|
304
|
+
for (const key of Object.keys(cliEnv)) {
|
|
305
|
+
if (/^ASDF_.+_VERSION$/.test(key)) delete cliEnv[key];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const child = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"], env: cliEnv });
|
|
309
|
+
|
|
310
|
+
writeFileSync(
|
|
311
|
+
runnerJsonPath,
|
|
312
|
+
JSON.stringify({
|
|
313
|
+
protocol: PROTOCOL,
|
|
314
|
+
runnerPid: process.pid,
|
|
315
|
+
cliPid: child.pid ?? null,
|
|
316
|
+
command,
|
|
317
|
+
args,
|
|
318
|
+
startedAt: Date.now(),
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
meta("spawn", { protocol: PROTOCOL, runnerPid: process.pid, cliPid: child.pid ?? null });
|
|
322
|
+
|
|
323
|
+
let stderrTail = "";
|
|
324
|
+
child.stderr?.setEncoding("utf8");
|
|
325
|
+
child.stderr?.on("data", (chunk) => {
|
|
326
|
+
stderrTail = (stderrTail + chunk).slice(-STDERR_CAP);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// CLI stdout → outbox, complete lines only (partial line buffered).
|
|
330
|
+
let stdoutBuf = "";
|
|
331
|
+
child.stdout?.setEncoding("utf8");
|
|
332
|
+
child.stdout?.on("data", (chunk) => {
|
|
333
|
+
stdoutBuf += chunk;
|
|
334
|
+
for (;;) {
|
|
335
|
+
const nl = stdoutBuf.indexOf("\n");
|
|
336
|
+
if (nl < 0) break;
|
|
337
|
+
const line = stdoutBuf.slice(0, nl);
|
|
338
|
+
stdoutBuf = stdoutBuf.slice(nl + 1);
|
|
339
|
+
if (line.trim().length > 0) outbox(line);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
let exiting = false;
|
|
344
|
+
child.on("exit", (code, signal) => {
|
|
345
|
+
if (exiting) return;
|
|
346
|
+
exiting = true;
|
|
347
|
+
if (stdoutBuf.trim().length > 0) outbox(stdoutBuf); // flush the partial tail
|
|
348
|
+
meta("exit", { code: connectionFailureSeen ? 1 : code, signal: signal ?? null, stderrTail });
|
|
349
|
+
process.exit(0);
|
|
350
|
+
});
|
|
351
|
+
child.on("error", (err) => {
|
|
352
|
+
if (exiting) return;
|
|
353
|
+
exiting = true;
|
|
354
|
+
meta("exit", { code: null, signal: null, stderrTail: String(err?.message ?? err) });
|
|
355
|
+
process.exit(0);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// ---- inbox tail → CLI stdin --------------------------------------------------
|
|
359
|
+
|
|
360
|
+
let inboxOffset = 0; // fresh session dir per spawn — always start at 0
|
|
361
|
+
let inboxBuf = "";
|
|
362
|
+
|
|
363
|
+
function stopCli() {
|
|
364
|
+
if (exiting) return;
|
|
365
|
+
try {
|
|
366
|
+
child.kill("SIGTERM");
|
|
367
|
+
} catch {
|
|
368
|
+
/* already gone */
|
|
369
|
+
}
|
|
370
|
+
setTimeout(() => {
|
|
371
|
+
try {
|
|
372
|
+
child.kill("SIGKILL");
|
|
373
|
+
} catch {
|
|
374
|
+
/* already gone */
|
|
375
|
+
}
|
|
376
|
+
}, STOP_GRACE_MS).unref();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function handleInboxLine(line) {
|
|
380
|
+
if (line.trim().length === 0) return;
|
|
381
|
+
if (line.startsWith('{"__uai"')) {
|
|
382
|
+
try {
|
|
383
|
+
const ctl = JSON.parse(line);
|
|
384
|
+
if (ctl.__uai === "stop") stopCli();
|
|
385
|
+
} catch {
|
|
386
|
+
/* malformed control — ignore */
|
|
387
|
+
}
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
child.stdin?.write(`${line}\n`);
|
|
392
|
+
} catch {
|
|
393
|
+
/* EPIPE after CLI death — the exit meta already tells the host */
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function pollInbox() {
|
|
398
|
+
let fd;
|
|
399
|
+
try {
|
|
400
|
+
fd = openSync(inboxPath, "r");
|
|
401
|
+
} catch {
|
|
402
|
+
return; // inbox not created yet
|
|
403
|
+
}
|
|
404
|
+
try {
|
|
405
|
+
const size = fstatSync(fd).size;
|
|
406
|
+
if (size > inboxOffset) {
|
|
407
|
+
const len = size - inboxOffset;
|
|
408
|
+
const buf = Buffer.alloc(len);
|
|
409
|
+
const read = readSync(fd, buf, 0, len, inboxOffset);
|
|
410
|
+
inboxOffset += read;
|
|
411
|
+
inboxBuf += buf.toString("utf8", 0, read);
|
|
412
|
+
for (;;) {
|
|
413
|
+
const nl = inboxBuf.indexOf("\n");
|
|
414
|
+
if (nl < 0) break;
|
|
415
|
+
const line = inboxBuf.slice(0, nl);
|
|
416
|
+
inboxBuf = inboxBuf.slice(nl + 1);
|
|
417
|
+
handleInboxLine(line);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
} finally {
|
|
421
|
+
closeSync(fd);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
setInterval(pollInbox, POLL_MS);
|
|
426
|
+
|
|
427
|
+
// ---- heartbeat ---------------------------------------------------------------
|
|
428
|
+
|
|
429
|
+
function beat() {
|
|
430
|
+
try {
|
|
431
|
+
writeFileSync(heartbeatPath, `${process.pid} ${Date.now()}\n`);
|
|
432
|
+
} catch {
|
|
433
|
+
/* disk hiccup — next beat retries */
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
beat();
|
|
437
|
+
setInterval(beat, HEARTBEAT_MS);
|
|
438
|
+
|
|
439
|
+
// The runner dies only with its CLI (or on stop/SIGTERM) — never because a
|
|
440
|
+
// host-side consumer went away. That is the entire point (ADR-061).
|
|
441
|
+
process.on("SIGTERM", stopCli);
|
|
442
|
+
process.on("SIGINT", stopCli);
|