@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
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClaudeSession — a real AgentSession backed by the Claude Code CLI in
|
|
3
|
+
* stream-json mode, run inside the task environment (ADR-010).
|
|
4
|
+
*
|
|
5
|
+
* claude --print --input-format stream-json --output-format stream-json \
|
|
6
|
+
* --verbose --no-session-persistence
|
|
7
|
+
*
|
|
8
|
+
* The CLI is a persistent bidirectional process: uai writes one JSON
|
|
9
|
+
* line per user turn to stdin and reads a stream of JSON event lines
|
|
10
|
+
* from stdout. Same binary, same subscription auth as the TUI — not the
|
|
11
|
+
* paid API (docs/interaction-model.md).
|
|
12
|
+
*
|
|
13
|
+
* Protocol mapping lives in the pure `mapClaudeLine` function so it can
|
|
14
|
+
* be unit-tested against fixture lines without spawning anything.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ VERIFY-ON-MAC: the exact stream-json envelope — especially the
|
|
17
|
+
* permission/control-request shape — must be checked against the
|
|
18
|
+
* installed `claude` build. The mapping is isolated here precisely so
|
|
19
|
+
* those fixups stay in one place.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { requireContainerRuntimeOperational } from "../runtime-guard";
|
|
23
|
+
import type { TaskEnvironmentAgentSessionSurface } from "../task-environment/types";
|
|
24
|
+
import { newId } from "../ulid";
|
|
25
|
+
import { createAgentTransport, type LineTransport } from "./transport";
|
|
26
|
+
import { rateLimitErrorUsage, resetSecondsToEpochMs } from "./rate-limit";
|
|
27
|
+
import { register } from "./registry";
|
|
28
|
+
import { CLAUDE_MODELS } from "../../src/engine-models";
|
|
29
|
+
import {
|
|
30
|
+
DISPATCH_INPUT_SCHEMA,
|
|
31
|
+
DISPATCH_RECIPIENT_ID_PATTERN,
|
|
32
|
+
DISPATCH_TOOL_NAME,
|
|
33
|
+
MAX_DISPATCH_INSTRUCTION_CHARS,
|
|
34
|
+
MAX_DISPATCH_RECIPIENT_ID_CHARS,
|
|
35
|
+
MAX_DISPATCH_RECIPIENTS,
|
|
36
|
+
parseDispatchInput,
|
|
37
|
+
type DispatchInput,
|
|
38
|
+
} from "./dispatch";
|
|
39
|
+
import { extractResultUsage } from "./usage";
|
|
40
|
+
import type {
|
|
41
|
+
AgentEvent,
|
|
42
|
+
AgentEventHandler,
|
|
43
|
+
AgentAccountUsage,
|
|
44
|
+
AgentKind,
|
|
45
|
+
AgentSession,
|
|
46
|
+
RosterAgent,
|
|
47
|
+
} from "./types";
|
|
48
|
+
|
|
49
|
+
// Published Claude model aliases the CLI accepts via `--model`. The CLI
|
|
50
|
+
// resolves these aliases to the current dated snapshots, so this list is
|
|
51
|
+
// stable across point releases. UPDATE WHEN MODELS CHANGE (new family or a
|
|
52
|
+
// retired alias). Order is display order in the cloud picker.
|
|
53
|
+
// The model list is host-agent/src/engine-models.ts (CLAUDE_MODELS).
|
|
54
|
+
|
|
55
|
+
// Opus ("opus" alias = Opus 4.8) stays the default; "fable" (Claude
|
|
56
|
+
// Fable 5, the Mythos-class flagship) is opt-in per agent. Update
|
|
57
|
+
// alongside CLAUDE_MODELS.
|
|
58
|
+
const CLAUDE_DEFAULT_MODEL = "opus";
|
|
59
|
+
|
|
60
|
+
// Reasoning levels passed through via `claude --effort <level>`. Taken from
|
|
61
|
+
// the CLI's `--help`: `--effort <level>` accepts (low, medium, high, xhigh,
|
|
62
|
+
// max). UPDATE WHEN THE CLI CHANGES its effort levels.
|
|
63
|
+
const CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
64
|
+
|
|
65
|
+
// High is the default reasoning level. Update alongside CLAUDE_EFFORTS.
|
|
66
|
+
const CLAUDE_DEFAULT_EFFORT = "high";
|
|
67
|
+
|
|
68
|
+
// ADR-083: cheap defaults apply at the enforcement boundary as well as in the
|
|
69
|
+
// picker. Explicit per-agent choices still win; an API client that omits them
|
|
70
|
+
// must not accidentally run the communicator on Claude's costly account
|
|
71
|
+
// defaults merely because it bypassed the web recommendation button.
|
|
72
|
+
const CLAUDE_COMMUNICATOR_MODEL = "haiku";
|
|
73
|
+
const CLAUDE_COMMUNICATOR_EFFORT = "low";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* ADR-083's one host-owned communicator tool. The server is deliberately
|
|
77
|
+
* dependency-free and rides in the adapter argv so an updated host can add it
|
|
78
|
+
* to an already-built task image. It performs no routing itself: Claude's
|
|
79
|
+
* structured tool_use block is the durable runner output, and ClaudeSession
|
|
80
|
+
* correlates its successful tool_result into the typed AgentEvent the
|
|
81
|
+
* orchestrator forwards.
|
|
82
|
+
*
|
|
83
|
+
* Keeping the MCP server a constant also keeps the strict config independent
|
|
84
|
+
* of project/user MCP files. The communicator gets exactly this server, never
|
|
85
|
+
* the task's arbitrary extension set.
|
|
86
|
+
*/
|
|
87
|
+
export const CLAUDE_DISPATCH_TOOL = `mcp__uai__${DISPATCH_TOOL_NAME}`;
|
|
88
|
+
|
|
89
|
+
export const CLAUDE_DISPATCH_MCP_SOURCE = String.raw`
|
|
90
|
+
import { createInterface } from "node:readline";
|
|
91
|
+
|
|
92
|
+
const RECIPIENT_ID = new RegExp(${JSON.stringify(DISPATCH_RECIPIENT_ID_PATTERN)});
|
|
93
|
+
|
|
94
|
+
const TOOL = {
|
|
95
|
+
name: "dispatch",
|
|
96
|
+
title: "Dispatch work to Moondeck crew agents",
|
|
97
|
+
description:
|
|
98
|
+
"Send one concrete instruction to one or more Moondeck crew agents. Use this instead of naming agents in prose; prose stays in the normal human-facing lane.",
|
|
99
|
+
inputSchema: ${JSON.stringify(DISPATCH_INPUT_SCHEMA)},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const write = (value) => process.stdout.write(JSON.stringify(value) + "\n");
|
|
103
|
+
const ok = (id, result) => write({ jsonrpc: "2.0", id, result });
|
|
104
|
+
const fail = (id, code, message) =>
|
|
105
|
+
write({ jsonrpc: "2.0", id, error: { code, message } });
|
|
106
|
+
const validArguments = (value) =>
|
|
107
|
+
value &&
|
|
108
|
+
typeof value === "object" &&
|
|
109
|
+
!Array.isArray(value) &&
|
|
110
|
+
Array.isArray(value.recipients) &&
|
|
111
|
+
value.recipients.length > 0 &&
|
|
112
|
+
value.recipients.length <= ${MAX_DISPATCH_RECIPIENTS} &&
|
|
113
|
+
value.recipients.every(
|
|
114
|
+
(recipient) =>
|
|
115
|
+
typeof recipient === "string" &&
|
|
116
|
+
recipient.length <= ${MAX_DISPATCH_RECIPIENT_ID_CHARS} &&
|
|
117
|
+
RECIPIENT_ID.test(recipient),
|
|
118
|
+
) &&
|
|
119
|
+
new Set(value.recipients).size === value.recipients.length &&
|
|
120
|
+
typeof value.instruction === "string" &&
|
|
121
|
+
value.instruction.trim().length > 0 &&
|
|
122
|
+
value.instruction.length <= ${MAX_DISPATCH_INSTRUCTION_CHARS};
|
|
123
|
+
|
|
124
|
+
createInterface({ input: process.stdin, crlfDelay: Infinity }).on(
|
|
125
|
+
"line",
|
|
126
|
+
(line) => {
|
|
127
|
+
let message;
|
|
128
|
+
try {
|
|
129
|
+
message = JSON.parse(line);
|
|
130
|
+
} catch {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!message || typeof message !== "object" || !("id" in message)) return;
|
|
134
|
+
if (message.method === "initialize") {
|
|
135
|
+
ok(message.id, {
|
|
136
|
+
protocolVersion: message.params?.protocolVersion ?? "2025-06-18",
|
|
137
|
+
capabilities: { tools: {} },
|
|
138
|
+
serverInfo: { name: "uai-dispatch", version: "1" },
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (message.method === "ping") {
|
|
143
|
+
ok(message.id, {});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (message.method === "tools/list") {
|
|
147
|
+
ok(message.id, { tools: [TOOL] });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (message.method === "tools/call") {
|
|
151
|
+
if (
|
|
152
|
+
message.params?.name !== "dispatch" ||
|
|
153
|
+
!validArguments(message.params?.arguments)
|
|
154
|
+
) {
|
|
155
|
+
ok(message.id, {
|
|
156
|
+
content: [{ type: "text", text: "Dispatch rejected: provide 1-${MAX_DISPATCH_RECIPIENTS} unique recipient ids and a non-empty instruction." }],
|
|
157
|
+
isError: true,
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
ok(message.id, {
|
|
162
|
+
content: [{ type: "text", text: "Dispatch request accepted by the host; Moondeck will validate recipients and delivery." }],
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
fail(message.id, -32601, "Method not found");
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
const CLAUDE_DISPATCH_MCP_CONFIG = JSON.stringify({
|
|
172
|
+
mcpServers: {
|
|
173
|
+
uai: {
|
|
174
|
+
command: "node",
|
|
175
|
+
args: ["--input-type=module", "--eval", CLAUDE_DISPATCH_MCP_SOURCE],
|
|
176
|
+
// The trusted bridge needs no task/provider credentials. Override the
|
|
177
|
+
// secrets the host intentionally forwards to Claude instead of enabling
|
|
178
|
+
// Claude's global subprocess scrub, which hard-requires bubblewrap and
|
|
179
|
+
// makes the CLI abort in existing task images that do not contain it.
|
|
180
|
+
env: {
|
|
181
|
+
CLAUDE_CODE_OAUTH_TOKEN: "",
|
|
182
|
+
ANTHROPIC_API_KEY: "",
|
|
183
|
+
ANTHROPIC_AUTH_TOKEN: "",
|
|
184
|
+
UAI_TASK_TOKEN: "",
|
|
185
|
+
},
|
|
186
|
+
// Claude defers MCP schemas by default. Dispatch is a core action named
|
|
187
|
+
// in the preamble, so it must be connected and visible on prompt one.
|
|
188
|
+
alwaysLoad: true,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const CLAUDE_COMMUNICATOR_SETTINGS = JSON.stringify({
|
|
194
|
+
disableAllHooks: true,
|
|
195
|
+
enabledPlugins: {},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* `--safe-mode` cannot be used here: Claude 2.1.220 disables even an explicit
|
|
200
|
+
* `--mcp-config` in safe mode, which makes the host-owned dispatch tool
|
|
201
|
+
* disappear. Recreate the customization lockdown while leaving that one
|
|
202
|
+
* strict MCP server available. These values override any agent/account env.
|
|
203
|
+
*/
|
|
204
|
+
const CLAUDE_COMMUNICATOR_ENV: Record<string, string> = {
|
|
205
|
+
CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1",
|
|
206
|
+
CLAUDE_CODE_DISABLE_BUNDLED_SKILLS: "1",
|
|
207
|
+
CLAUDE_CODE_DISABLE_CLAUDE_MDS: "1",
|
|
208
|
+
CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL: "1",
|
|
209
|
+
CLAUDE_CODE_DISABLE_POLICY_SKILLS: "1",
|
|
210
|
+
CLAUDE_CODE_DISABLE_WORKFLOWS: "1",
|
|
211
|
+
CLAUDE_CODE_SKIP_PLUGIN_MCP_SERVERS: "1",
|
|
212
|
+
CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS: "1",
|
|
213
|
+
ENABLE_CLAUDEAI_MCP_SERVERS: "false",
|
|
214
|
+
// Some hosts export this globally. Claude's current implementation aborts
|
|
215
|
+
// when it is enabled but the task image has no bubblewrap binary.
|
|
216
|
+
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: "",
|
|
217
|
+
// The enforced profile dispatches through its host-owned MCP event, not the
|
|
218
|
+
// task CLI. Keep the bearer token and API origin out of the Claude process
|
|
219
|
+
// itself (not merely its MCP child), where Read could otherwise inspect
|
|
220
|
+
// /proc/self/environ and recover a permission-null task credential.
|
|
221
|
+
UAI_TASK_TOKEN: "",
|
|
222
|
+
UAI_API_URL: "",
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Pure protocol mapping — stream-json line → AgentEvent[].
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
/** Narrow an unknown to a plain object. */
|
|
230
|
+
function isObj(v: unknown): v is Record<string, unknown> {
|
|
231
|
+
return typeof v === "object" && v !== null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function parseLineObject(raw: string): Record<string, unknown> | null {
|
|
235
|
+
try {
|
|
236
|
+
const value: unknown = JSON.parse(raw);
|
|
237
|
+
return isObj(value) ? value : null;
|
|
238
|
+
} catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Map one stream-json stdout line to zero or more AgentEvents.
|
|
245
|
+
*
|
|
246
|
+
* Contract:
|
|
247
|
+
* - `stream_event` content_block_delta(text) → `message_delta`
|
|
248
|
+
* - `assistant` message tool_use blocks → `tool_call`
|
|
249
|
+
* - `result` → `message_complete` + `turn_complete`
|
|
250
|
+
* - control/permission request → `permission_request`
|
|
251
|
+
* - anything else → [] (ignored)
|
|
252
|
+
*
|
|
253
|
+
* A malformed line yields []. The mapper is intentionally total and
|
|
254
|
+
* side-effect free.
|
|
255
|
+
*/
|
|
256
|
+
export function mapClaudeLine(raw: string, now: number = Date.now()): AgentEvent[] {
|
|
257
|
+
const json = parseLineObject(raw);
|
|
258
|
+
if (!json) return [];
|
|
259
|
+
|
|
260
|
+
const type = json.type;
|
|
261
|
+
|
|
262
|
+
// Claude Code 2.1.306 emits this exact stream-json envelope. Its embedded
|
|
263
|
+
// schema defines reset timestamps as Unix seconds and utilization as a
|
|
264
|
+
// fraction (normally 0..1).
|
|
265
|
+
if (type === "rate_limit_event" && isObj(json.rate_limit_info)) {
|
|
266
|
+
const usage = mapClaudeRateLimitInfo(json.rate_limit_info, now);
|
|
267
|
+
return usage ? [{ type: "account_usage", usage }] : [];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// --- streaming text deltas --------------------------------------------
|
|
271
|
+
if (type === "stream_event" && isObj(json.event)) {
|
|
272
|
+
const event = json.event;
|
|
273
|
+
if (
|
|
274
|
+
event.type === "content_block_delta" &&
|
|
275
|
+
isObj(event.delta) &&
|
|
276
|
+
event.delta.type === "text_delta" &&
|
|
277
|
+
typeof event.delta.text === "string"
|
|
278
|
+
) {
|
|
279
|
+
return [{ type: "message_delta", text: event.delta.text }];
|
|
280
|
+
}
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// --- full assistant message: harvest tool calls -----------------------
|
|
285
|
+
if (type === "assistant" && isObj(json.message)) {
|
|
286
|
+
const content = json.message.content;
|
|
287
|
+
if (!Array.isArray(content)) return [];
|
|
288
|
+
const out: AgentEvent[] = [];
|
|
289
|
+
for (const block of content) {
|
|
290
|
+
if (isObj(block) && block.type === "tool_use") {
|
|
291
|
+
const name = typeof block.name === "string" ? block.name : "tool";
|
|
292
|
+
if (name === CLAUDE_DISPATCH_TOOL) {
|
|
293
|
+
// Dispatch is intentionally absent here. Claude emits this assistant
|
|
294
|
+
// block before it calls the MCP server; ClaudeSession correlates the
|
|
295
|
+
// later tool_result and emits only after the call succeeds.
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
out.push({
|
|
299
|
+
type: "tool_call",
|
|
300
|
+
id: typeof block.id === "string" ? block.id : newId(),
|
|
301
|
+
title: name,
|
|
302
|
+
detail: safeStringify(block.input),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return out;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// --- turn result ------------------------------------------------------
|
|
310
|
+
if (type === "result") {
|
|
311
|
+
const text = typeof json.result === "string" ? json.result : "";
|
|
312
|
+
const usage = extractResultUsage(json);
|
|
313
|
+
if (json.is_error === true) {
|
|
314
|
+
// An errored turn still cost tokens — meter it.
|
|
315
|
+
const message = text || "claude returned an error";
|
|
316
|
+
const accountUsage = rateLimitErrorUsage(message, now);
|
|
317
|
+
return [
|
|
318
|
+
...(accountUsage
|
|
319
|
+
? [{ type: "account_usage", usage: accountUsage } as const]
|
|
320
|
+
: []),
|
|
321
|
+
// ADR-076: a rate-limit/usage-cap error is failed over to another
|
|
322
|
+
// account by the orchestrator rather than surfaced to the user.
|
|
323
|
+
accountUsage
|
|
324
|
+
? {
|
|
325
|
+
type: "error",
|
|
326
|
+
message,
|
|
327
|
+
retryable: "rate_limit",
|
|
328
|
+
...(accountUsage.resetsAt === undefined
|
|
329
|
+
? {}
|
|
330
|
+
: { resetsAt: accountUsage.resetsAt }),
|
|
331
|
+
}
|
|
332
|
+
: { type: "error", message },
|
|
333
|
+
{ type: "turn_complete", usage },
|
|
334
|
+
];
|
|
335
|
+
}
|
|
336
|
+
return [
|
|
337
|
+
{ type: "message_complete", text },
|
|
338
|
+
{ type: "turn_complete", usage },
|
|
339
|
+
];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// --- permission / control request -------------------------------------
|
|
343
|
+
// ⚠️ VERIFY-ON-MAC: confirm this envelope against the real CLI.
|
|
344
|
+
if (type === "control_request" && isObj(json.request)) {
|
|
345
|
+
const req = json.request;
|
|
346
|
+
if (req.subtype === "can_use_tool") {
|
|
347
|
+
return [
|
|
348
|
+
{
|
|
349
|
+
type: "permission_request",
|
|
350
|
+
id: typeof json.request_id === "string" ? json.request_id : newId(),
|
|
351
|
+
title:
|
|
352
|
+
typeof req.tool_name === "string"
|
|
353
|
+
? `Allow ${req.tool_name}?`
|
|
354
|
+
: "Permission requested",
|
|
355
|
+
detail: safeStringify(req.input),
|
|
356
|
+
},
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const CLAUDE_WINDOW_MINUTES: Readonly<Record<string, number>> = {
|
|
366
|
+
five_hour: 5 * 60,
|
|
367
|
+
seven_day: 7 * 24 * 60,
|
|
368
|
+
seven_day_opus: 7 * 24 * 60,
|
|
369
|
+
seven_day_sonnet: 7 * 24 * 60,
|
|
370
|
+
seven_day_overage_included: 7 * 24 * 60,
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
function utilizationPercent(value: unknown): number | undefined {
|
|
374
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
return Math.min(100, value * 100);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Map Claude Code 2.1.306's embedded `RateLimitInfo` schema. */
|
|
381
|
+
export function mapClaudeRateLimitInfo(
|
|
382
|
+
info: Record<string, unknown>,
|
|
383
|
+
observedAt: number = Date.now(),
|
|
384
|
+
): AgentAccountUsage | null {
|
|
385
|
+
const status =
|
|
386
|
+
info.status === "allowed"
|
|
387
|
+
? "allowed"
|
|
388
|
+
: info.status === "allowed_warning"
|
|
389
|
+
? "warning"
|
|
390
|
+
: info.status === "rejected"
|
|
391
|
+
? "limited"
|
|
392
|
+
: null;
|
|
393
|
+
if (!status) return null;
|
|
394
|
+
|
|
395
|
+
const windows: AgentAccountUsage["windows"] = [];
|
|
396
|
+
const seen = new Set<string>();
|
|
397
|
+
if (isObj(info.unifiedWindows)) {
|
|
398
|
+
for (const window of [
|
|
399
|
+
"five_hour",
|
|
400
|
+
"seven_day",
|
|
401
|
+
"seven_day_overage_included",
|
|
402
|
+
] as const) {
|
|
403
|
+
const value = info.unifiedWindows[window];
|
|
404
|
+
if (!isObj(value)) continue;
|
|
405
|
+
const usedPercent = utilizationPercent(value.utilization);
|
|
406
|
+
const resetsAt = resetSecondsToEpochMs(value.resetsAt, observedAt);
|
|
407
|
+
windows.push({
|
|
408
|
+
window,
|
|
409
|
+
windowMinutes: CLAUDE_WINDOW_MINUTES[window],
|
|
410
|
+
...(usedPercent === undefined ? {} : { usedPercent }),
|
|
411
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
412
|
+
});
|
|
413
|
+
seen.add(window);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const rateLimitType =
|
|
418
|
+
typeof info.rateLimitType === "string" &&
|
|
419
|
+
info.rateLimitType.length > 0 &&
|
|
420
|
+
info.rateLimitType.length <= 64
|
|
421
|
+
? info.rateLimitType
|
|
422
|
+
: undefined;
|
|
423
|
+
const resetsAt = resetSecondsToEpochMs(info.resetsAt, observedAt);
|
|
424
|
+
if (rateLimitType && !seen.has(rateLimitType)) {
|
|
425
|
+
const usedPercent = utilizationPercent(info.utilization);
|
|
426
|
+
windows.push({
|
|
427
|
+
window: rateLimitType,
|
|
428
|
+
...(CLAUDE_WINDOW_MINUTES[rateLimitType] === undefined
|
|
429
|
+
? {}
|
|
430
|
+
: { windowMinutes: CLAUDE_WINDOW_MINUTES[rateLimitType] }),
|
|
431
|
+
...(usedPercent === undefined ? {} : { usedPercent }),
|
|
432
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
status,
|
|
438
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
439
|
+
...(typeof info.isUsingOverage === "boolean"
|
|
440
|
+
? { usingOverage: info.isUsingOverage }
|
|
441
|
+
: {}),
|
|
442
|
+
windows,
|
|
443
|
+
source: "claude-rate-limit-event",
|
|
444
|
+
observedAt,
|
|
445
|
+
...(rateLimitType === undefined
|
|
446
|
+
? {}
|
|
447
|
+
: { coverage: { kind: "window" as const, window: rateLimitType } }),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function safeStringify(v: unknown): string {
|
|
452
|
+
if (v === undefined) return "";
|
|
453
|
+
if (typeof v === "string") return v;
|
|
454
|
+
try {
|
|
455
|
+
return JSON.stringify(v, null, 2);
|
|
456
|
+
} catch {
|
|
457
|
+
return String(v);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
// The session.
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
|
|
465
|
+
const CLAUDE_BASE_ARGS = [
|
|
466
|
+
"--print",
|
|
467
|
+
"--input-format",
|
|
468
|
+
"stream-json",
|
|
469
|
+
"--output-format",
|
|
470
|
+
"stream-json",
|
|
471
|
+
"--verbose",
|
|
472
|
+
// uai owns durable chat persistence. Claude Code's JSON transcript
|
|
473
|
+
// files are an implementation detail and can contain redacted
|
|
474
|
+
// thinking blocks. Replaying those from disk has caused Anthropic API
|
|
475
|
+
// 400s when a later turn sees a changed thinking block, so keep each
|
|
476
|
+
// managed stream-json process in-memory only.
|
|
477
|
+
"--no-session-persistence",
|
|
478
|
+
];
|
|
479
|
+
|
|
480
|
+
const CLAUDE_FULL_ACCESS_ARGS = [
|
|
481
|
+
...CLAUDE_BASE_ARGS,
|
|
482
|
+
// Full tool access, no per-call prompts. Safe here precisely because
|
|
483
|
+
// a uai task runs in a throwaway, isolated container operating on a
|
|
484
|
+
// disposable worktree (ADR-001 / ADR-010) — the container *is* the
|
|
485
|
+
// sandbox. Without this, stream-json has no interactive approver and
|
|
486
|
+
// every Write/Bash silently self-denies.
|
|
487
|
+
"--dangerously-skip-permissions",
|
|
488
|
+
// MCP servers come from the uai-managed file, EXPLICITLY (ADR-057/061).
|
|
489
|
+
// Discovered project .mcp.json servers sit behind a per-server approval
|
|
490
|
+
// that headless --print can never answer ("⏸ Pending approval"), which
|
|
491
|
+
// silently loaded a stale subset. --mcp-config bypasses the approval
|
|
492
|
+
// gate; --strict-mcp-config keeps stray host-copied user configs out.
|
|
493
|
+
// setupMcpTaskConfig always writes this file (empty when no connections)
|
|
494
|
+
// before sessions spawn.
|
|
495
|
+
"--mcp-config",
|
|
496
|
+
"/workspace/.mcp.json",
|
|
497
|
+
"--strict-mcp-config",
|
|
498
|
+
];
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* ADR-157 Astronaut profile: the normal tool set, minus Claude Code's native
|
|
502
|
+
* subagents. An Astronaut's helpers must be crew agents the task can see and
|
|
503
|
+
* bill; a built-in Agent/Task subagent would run (and spend) outside the crew.
|
|
504
|
+
*/
|
|
505
|
+
const CLAUDE_ASTRONAUT_ARGS = [
|
|
506
|
+
...CLAUDE_FULL_ACCESS_ARGS,
|
|
507
|
+
"--disallowedTools",
|
|
508
|
+
"Agent,Task",
|
|
509
|
+
];
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* ADR-083 communicator profile. This is deliberately an engine-level tool
|
|
513
|
+
* boundary, not prompt advice: setting sources, hooks, skills, workflows and
|
|
514
|
+
* plug-in MCP are suppressed; the explicit tool set contains no shell or
|
|
515
|
+
* mutation primitive; `dontAsk` denies anything outside it; and strict MCP
|
|
516
|
+
* exposes only Uai's host-owned dispatch tool.
|
|
517
|
+
*/
|
|
518
|
+
const CLAUDE_COMMUNICATOR_ARGS = [
|
|
519
|
+
...CLAUDE_BASE_ARGS,
|
|
520
|
+
"--setting-sources",
|
|
521
|
+
"",
|
|
522
|
+
"--settings",
|
|
523
|
+
CLAUDE_COMMUNICATOR_SETTINGS,
|
|
524
|
+
"--disable-slash-commands",
|
|
525
|
+
"--no-chrome",
|
|
526
|
+
"--permission-mode",
|
|
527
|
+
"dontAsk",
|
|
528
|
+
"--tools",
|
|
529
|
+
"Read,Glob,Grep",
|
|
530
|
+
"--allowedTools",
|
|
531
|
+
CLAUDE_DISPATCH_TOOL,
|
|
532
|
+
"--disallowedTools",
|
|
533
|
+
"Bash,Edit,Write,NotebookEdit,Agent,Task,WebFetch,WebSearch",
|
|
534
|
+
"--mcp-config",
|
|
535
|
+
CLAUDE_DISPATCH_MCP_CONFIG,
|
|
536
|
+
"--strict-mcp-config",
|
|
537
|
+
];
|
|
538
|
+
|
|
539
|
+
export class ClaudeSession implements AgentSession {
|
|
540
|
+
readonly agentId: string;
|
|
541
|
+
readonly kind: AgentKind = "claude";
|
|
542
|
+
|
|
543
|
+
private readonly proc: LineTransport;
|
|
544
|
+
private readonly handlers = new Set<AgentEventHandler>();
|
|
545
|
+
private readonly pendingDispatches = new Map<string, DispatchInput>();
|
|
546
|
+
private readonly settledDispatches = new Set<string>();
|
|
547
|
+
private closed = false;
|
|
548
|
+
// Claude's `total_cost_usd` is CUMULATIVE across the persistent session
|
|
549
|
+
// process (ADR-061 keeps one claude alive across turns), so per-turn cost is
|
|
550
|
+
// the delta from the previous turn — otherwise summing turns over-counts
|
|
551
|
+
// wildly (live 2026-07-22: a 4-turn task billed ~4x). Token counts, by
|
|
552
|
+
// contrast, are per-turn already. A fresh CLI starts at zero; attaching an
|
|
553
|
+
// existing durable CLI restores its counter at the saved outbox checkpoint.
|
|
554
|
+
// Undefined means the bounded history could not prove a baseline: skip one
|
|
555
|
+
// estimate instead of rebilling the session's whole previous spend.
|
|
556
|
+
private lastCumulativeCostUsd: number | undefined = 0;
|
|
557
|
+
private costSessionId: string | undefined;
|
|
558
|
+
private readonly costBySession = new Map<string, number>();
|
|
559
|
+
|
|
560
|
+
constructor(args: {
|
|
561
|
+
taskId: string;
|
|
562
|
+
agent: RosterAgent;
|
|
563
|
+
environment: TaskEnvironmentAgentSessionSurface;
|
|
564
|
+
systemPreamble: string;
|
|
565
|
+
executionProfile?: "communicator" | "astronaut";
|
|
566
|
+
attachCompatibilityKey?: string;
|
|
567
|
+
agentEnv?: Record<string, string>;
|
|
568
|
+
claudeSettings?: Record<string, unknown>;
|
|
569
|
+
}) {
|
|
570
|
+
this.agentId = args.agent.id;
|
|
571
|
+
|
|
572
|
+
// The uai channel briefing (how to @-mention, the agent roster, the
|
|
573
|
+
// project's defaultPrompt) is passed as a real system prompt via
|
|
574
|
+
// `--append-system-prompt`, so it applies to every turn — not
|
|
575
|
+
// smuggled into the first user message.
|
|
576
|
+
const cliArgs = [
|
|
577
|
+
...(args.executionProfile === "communicator"
|
|
578
|
+
? CLAUDE_COMMUNICATOR_ARGS
|
|
579
|
+
: args.executionProfile === "astronaut"
|
|
580
|
+
? CLAUDE_ASTRONAUT_ARGS
|
|
581
|
+
: CLAUDE_FULL_ACCESS_ARGS),
|
|
582
|
+
];
|
|
583
|
+
if (args.claudeSettings) {
|
|
584
|
+
// Empty helper settings and fallbackModel override inherited values.
|
|
585
|
+
// Preserve native settings sources, including project hooks/plugins.
|
|
586
|
+
// Connection env separately pins routing with PROVIDER_MANAGED_BY_HOST.
|
|
587
|
+
const settings = {
|
|
588
|
+
...(args.executionProfile === "communicator" ? JSON.parse(CLAUDE_COMMUNICATOR_SETTINGS) as Record<string, unknown> : {}),
|
|
589
|
+
...args.claudeSettings,
|
|
590
|
+
};
|
|
591
|
+
cliArgs.push("--settings", JSON.stringify(settings));
|
|
592
|
+
}
|
|
593
|
+
if (process.env.UAI_CLAUDE_INCLUDE_PARTIAL_MESSAGES === "1") {
|
|
594
|
+
cliArgs.push("--include-partial-messages");
|
|
595
|
+
}
|
|
596
|
+
// Explicit task configuration wins; the communicator uses its declared
|
|
597
|
+
// fast/cheap defaults when the task left either choice unspecified.
|
|
598
|
+
const model =
|
|
599
|
+
args.agent.model ??
|
|
600
|
+
(args.executionProfile === "communicator"
|
|
601
|
+
? CLAUDE_COMMUNICATOR_MODEL
|
|
602
|
+
: undefined);
|
|
603
|
+
if (model) {
|
|
604
|
+
cliArgs.push("--model", model);
|
|
605
|
+
}
|
|
606
|
+
const effort =
|
|
607
|
+
args.agent.effort ??
|
|
608
|
+
(args.executionProfile === "communicator"
|
|
609
|
+
? CLAUDE_COMMUNICATOR_EFFORT
|
|
610
|
+
: undefined);
|
|
611
|
+
if (effort) {
|
|
612
|
+
cliArgs.push("--effort", effort);
|
|
613
|
+
}
|
|
614
|
+
if (args.systemPreamble.trim().length > 0) {
|
|
615
|
+
cliArgs.push("--append-system-prompt", args.systemPreamble);
|
|
616
|
+
}
|
|
617
|
+
// Forward host-resident Claude auth into the headless exec. Headless
|
|
618
|
+
// `--print` does NOT use the interactive subscription/keychain path, so
|
|
619
|
+
// it needs CLAUDE_CODE_OAUTH_TOKEN (from `claude setup-token`) or an
|
|
620
|
+
// API key. These live in the host-agent's env (never the cloud, ADR-015);
|
|
621
|
+
// only ones actually set are forwarded.
|
|
622
|
+
// ADR-061: durable by default — the CLI is owned by an in-container
|
|
623
|
+
// runner and survives host restarts (attach resumes it); legacy pipes
|
|
624
|
+
// behind UAI_DURABLE_SESSIONS=0. Claude is host-side stateless, so a
|
|
625
|
+
// live runner can be re-attached (allowAttach). A communicator profile is
|
|
626
|
+
// never attached, and the orchestrator also supplies a persisted policy
|
|
627
|
+
// compatibility key for normal-tools Secretaries. That second fence makes
|
|
628
|
+
// the reverse rolling upgrade safe: an old restricted runner cannot be
|
|
629
|
+
// inherited after the default policy changes back to normal tools.
|
|
630
|
+
this.proc = createAgentTransport({
|
|
631
|
+
taskId: args.taskId,
|
|
632
|
+
agentId: this.agentId,
|
|
633
|
+
environment: args.environment,
|
|
634
|
+
cli: "claude",
|
|
635
|
+
cliArgs,
|
|
636
|
+
passEnv: ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
|
|
637
|
+
explicitEnv:
|
|
638
|
+
args.executionProfile === "communicator"
|
|
639
|
+
? { ...(args.agentEnv ?? {}), ...CLAUDE_COMMUNICATOR_ENV }
|
|
640
|
+
: (args.agentEnv ?? {}),
|
|
641
|
+
allowAttach: args.executionProfile !== "communicator",
|
|
642
|
+
attachCompatibilityKey: args.attachCompatibilityKey,
|
|
643
|
+
kind: "claude",
|
|
644
|
+
debugLabel: `claude:${this.agentId}`,
|
|
645
|
+
restoreProtocolState: {
|
|
646
|
+
accepts: (line) => {
|
|
647
|
+
const json = parseLineObject(line);
|
|
648
|
+
return json?.type === "result" && this.validCumulativeCost(json) !== undefined;
|
|
649
|
+
},
|
|
650
|
+
restore: (lines, complete) => {
|
|
651
|
+
this.lastCumulativeCostUsd = complete ? 0 : undefined;
|
|
652
|
+
this.costSessionId = undefined;
|
|
653
|
+
this.costBySession.clear();
|
|
654
|
+
for (const line of lines) {
|
|
655
|
+
const json = parseLineObject(line);
|
|
656
|
+
if (json) this.perTurnCost({ type: "turn_complete", usage: { costUsd: this.validCumulativeCost(json) } }, json);
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
this.proc.onLine((line) => {
|
|
662
|
+
for (const event of this.dispatchEventsForLine(line)) this.emit(event);
|
|
663
|
+
const json = parseLineObject(line);
|
|
664
|
+
for (const event of mapClaudeLine(line)) this.emit(this.perTurnCost(event, json));
|
|
665
|
+
});
|
|
666
|
+
this.proc.onExit((code) => {
|
|
667
|
+
if (this.closed) return;
|
|
668
|
+
const tail = this.proc.stderrTail.trim();
|
|
669
|
+
// Surface an error event when:
|
|
670
|
+
// - the process exited non-zero, OR
|
|
671
|
+
// - it exited 0 but stderr says "Claude configuration file not
|
|
672
|
+
// found". That message means Claude silently bailed because
|
|
673
|
+
// its config was unlinked (Docker Desktop macOS atomic-write
|
|
674
|
+
// race). The orchestrator handles this by repairing the
|
|
675
|
+
// config and respawning, but only if it sees the error event.
|
|
676
|
+
const configMissing =
|
|
677
|
+
/Claude configuration file not found/i.test(tail);
|
|
678
|
+
if (code !== 0 || configMissing) {
|
|
679
|
+
this.emit({
|
|
680
|
+
type: "error",
|
|
681
|
+
message:
|
|
682
|
+
`claude process exited (${code ?? "spawn failed"})` +
|
|
683
|
+
(tail
|
|
684
|
+
? `:\n${tail}`
|
|
685
|
+
: " — no stderr. Is the claude CLI installed in the task container, and is the container running?"),
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
this.clearDispatches();
|
|
689
|
+
this.closed = true;
|
|
690
|
+
this.emit({ type: "exit", code: code ?? -1 });
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
onEvent(handler: AgentEventHandler): () => void {
|
|
695
|
+
this.handlers.add(handler);
|
|
696
|
+
return () => this.handlers.delete(handler);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private emit(event: AgentEvent): void {
|
|
700
|
+
if (this.closed && event.type !== "exit") return;
|
|
701
|
+
for (const h of this.handlers) h(event);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Claude writes an assistant tool_use before invoking MCP, then a user
|
|
706
|
+
* tool_result carrying the same id. Waking crew on the first line would turn
|
|
707
|
+
* a rejected or failed tool call into real work, so hold validated input
|
|
708
|
+
* until the matching result succeeds. The settled set makes replayed output
|
|
709
|
+
* idempotent for the remainder of the turn.
|
|
710
|
+
*/
|
|
711
|
+
private dispatchEventsForLine(raw: string): AgentEvent[] {
|
|
712
|
+
const json = parseLineObject(raw);
|
|
713
|
+
if (!json) return [];
|
|
714
|
+
|
|
715
|
+
if (json.type === "assistant" && isObj(json.message)) {
|
|
716
|
+
const content = json.message.content;
|
|
717
|
+
if (!Array.isArray(content)) return [];
|
|
718
|
+
for (const block of content) {
|
|
719
|
+
if (
|
|
720
|
+
!isObj(block) ||
|
|
721
|
+
block.type !== "tool_use" ||
|
|
722
|
+
block.name !== CLAUDE_DISPATCH_TOOL ||
|
|
723
|
+
typeof block.id !== "string" ||
|
|
724
|
+
this.settledDispatches.has(block.id)
|
|
725
|
+
) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const input = parseDispatchInput(block.input);
|
|
729
|
+
if (input) this.pendingDispatches.set(block.id, input);
|
|
730
|
+
else this.pendingDispatches.delete(block.id);
|
|
731
|
+
}
|
|
732
|
+
return [];
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (json.type === "user" && isObj(json.message)) {
|
|
736
|
+
const content = json.message.content;
|
|
737
|
+
if (!Array.isArray(content)) return [];
|
|
738
|
+
const events: AgentEvent[] = [];
|
|
739
|
+
for (const block of content) {
|
|
740
|
+
if (
|
|
741
|
+
!isObj(block) ||
|
|
742
|
+
block.type !== "tool_result" ||
|
|
743
|
+
typeof block.tool_use_id !== "string"
|
|
744
|
+
) {
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
const pending = this.pendingDispatches.get(block.tool_use_id);
|
|
748
|
+
if (!pending) continue;
|
|
749
|
+
this.pendingDispatches.delete(block.tool_use_id);
|
|
750
|
+
this.settledDispatches.add(block.tool_use_id);
|
|
751
|
+
if (block.is_error !== true) {
|
|
752
|
+
events.push({
|
|
753
|
+
type: "dispatch",
|
|
754
|
+
dispatchId: block.tool_use_id,
|
|
755
|
+
...pending,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return events;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (json.type === "result") this.clearDispatches();
|
|
763
|
+
return [];
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
private clearDispatches(): void {
|
|
767
|
+
this.pendingDispatches.clear();
|
|
768
|
+
this.settledDispatches.clear();
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private validCumulativeCost(json: Record<string, unknown>): number | undefined {
|
|
772
|
+
const cost = json.total_cost_usd;
|
|
773
|
+
// A synthetic crash result may carry zeroed counters. It cannot erase a
|
|
774
|
+
// prior baseline or make the following result rebill the whole session.
|
|
775
|
+
return typeof cost === "number" && Number.isFinite(cost) && cost >= 0 &&
|
|
776
|
+
!(cost === 0 && json.is_error === true) ? cost : undefined;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** SDK streaming results carry session-cumulative estimated USD; usage
|
|
780
|
+
* tokens are per-turn. /clear, /reset and /new change session_id and reset
|
|
781
|
+
* cost even when the new counter already exceeds the old one. */
|
|
782
|
+
private perTurnCost(event: AgentEvent, json: Record<string, unknown> | null): AgentEvent {
|
|
783
|
+
if (event.type !== "turn_complete" || event.usage?.costUsd === undefined) {
|
|
784
|
+
return event;
|
|
785
|
+
}
|
|
786
|
+
const cumulative = json && this.validCumulativeCost(json);
|
|
787
|
+
const withoutCost = () => {
|
|
788
|
+
const { costUsd: _cost, ...usage } = event.usage!;
|
|
789
|
+
return { ...event, usage };
|
|
790
|
+
};
|
|
791
|
+
if (cumulative === null || cumulative === undefined) return withoutCost();
|
|
792
|
+
const sessionId = typeof json?.session_id === "string" ? json.session_id : undefined;
|
|
793
|
+
const previous = sessionId !== undefined && this.costBySession.has(sessionId)
|
|
794
|
+
? this.costBySession.get(sessionId)
|
|
795
|
+
: sessionId !== undefined && this.costSessionId !== undefined && sessionId !== this.costSessionId
|
|
796
|
+
? 0 : this.lastCumulativeCostUsd;
|
|
797
|
+
// A decreasing counter in the same identified session is stale/replayed
|
|
798
|
+
// output, not proof of a reset. Keep the last known baseline.
|
|
799
|
+
if (previous !== undefined && cumulative < previous && sessionId !== undefined && this.costBySession.has(sessionId)) {
|
|
800
|
+
return withoutCost();
|
|
801
|
+
}
|
|
802
|
+
this.lastCumulativeCostUsd = cumulative;
|
|
803
|
+
this.costSessionId = sessionId ?? this.costSessionId;
|
|
804
|
+
if (sessionId !== undefined) this.costBySession.set(sessionId, cumulative);
|
|
805
|
+
if (previous === undefined) return withoutCost();
|
|
806
|
+
const delta = cumulative - previous;
|
|
807
|
+
// Older result envelopes without session_id retain the counter-reset
|
|
808
|
+
// fallback; current SDK frames identify that boundary explicitly.
|
|
809
|
+
return {
|
|
810
|
+
...event,
|
|
811
|
+
usage: { ...event.usage, costUsd: delta >= 0 ? delta : cumulative },
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async send(text: string): Promise<void> {
|
|
816
|
+
if (this.closed) return;
|
|
817
|
+
// The transport can outlive the command that created it. Gate each new
|
|
818
|
+
// turn at its actual runner boundary, while still allowing interrupt and
|
|
819
|
+
// close traffic during runtime recovery.
|
|
820
|
+
requireContainerRuntimeOperational();
|
|
821
|
+
this.proc.writeLine({
|
|
822
|
+
type: "user",
|
|
823
|
+
message: { role: "user", content: text },
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async interrupt(): Promise<void> {
|
|
828
|
+
if (this.closed) return;
|
|
829
|
+
// Claude Code's stream-json control channel: an `interrupt` control-request
|
|
830
|
+
// stops the in-flight turn (the SDK's ESC). Safe to send when idle — the
|
|
831
|
+
// CLI acks with a control_response we don't need to track.
|
|
832
|
+
// ⚠️ VERIFY-ON-MAC: confirm the control-request `interrupt` envelope.
|
|
833
|
+
this.proc.writeLine({
|
|
834
|
+
type: "control_request",
|
|
835
|
+
request_id: newId(),
|
|
836
|
+
request: { subtype: "interrupt" },
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async resolvePermission(
|
|
841
|
+
requestId: string,
|
|
842
|
+
decision: "accept" | "decline",
|
|
843
|
+
): Promise<void> {
|
|
844
|
+
if (this.closed) return;
|
|
845
|
+
// ⚠️ VERIFY-ON-MAC: confirm the control-response envelope.
|
|
846
|
+
this.proc.writeLine({
|
|
847
|
+
type: "control_response",
|
|
848
|
+
request_id: requestId,
|
|
849
|
+
response: {
|
|
850
|
+
subtype: "can_use_tool",
|
|
851
|
+
behavior: decision === "accept" ? "allow" : "deny",
|
|
852
|
+
},
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async close(): Promise<void> {
|
|
857
|
+
if (this.closed) {
|
|
858
|
+
await this.proc.close();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
this.closed = true;
|
|
862
|
+
this.clearDispatches();
|
|
863
|
+
await this.proc.close();
|
|
864
|
+
this.emit({ type: "exit", code: 0 });
|
|
865
|
+
this.handlers.clear();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Register the Claude adapter at module load (ADR-021). The `--model`
|
|
870
|
+
// pass-through above means `model` flows from the roster entry to the CLI.
|
|
871
|
+
register({
|
|
872
|
+
kind: "claude",
|
|
873
|
+
label: "Claude",
|
|
874
|
+
supportedModels: () => [...CLAUDE_MODELS],
|
|
875
|
+
defaultModel: CLAUDE_DEFAULT_MODEL,
|
|
876
|
+
supportedEfforts: () => [...CLAUDE_EFFORTS],
|
|
877
|
+
defaultEffort: CLAUDE_DEFAULT_EFFORT,
|
|
878
|
+
executionProfiles: [
|
|
879
|
+
{
|
|
880
|
+
id: "communicator",
|
|
881
|
+
mechanism: "claude-explicit-tool-boundary-v2",
|
|
882
|
+
defaultModel: CLAUDE_COMMUNICATOR_MODEL,
|
|
883
|
+
defaultEffort: CLAUDE_COMMUNICATOR_EFFORT,
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
id: "astronaut",
|
|
887
|
+
mechanism: "claude-disallowed-agent-task-v1",
|
|
888
|
+
},
|
|
889
|
+
],
|
|
890
|
+
// Usable only when a Claude credential is in the env (injected into task
|
|
891
|
+
// containers at task-up). Gates advertisement (ADR-044 P2).
|
|
892
|
+
available: () =>
|
|
893
|
+
Boolean(
|
|
894
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN ||
|
|
895
|
+
process.env.ANTHROPIC_API_KEY ||
|
|
896
|
+
process.env.ANTHROPIC_AUTH_TOKEN,
|
|
897
|
+
),
|
|
898
|
+
create: async ({
|
|
899
|
+
taskId,
|
|
900
|
+
agent,
|
|
901
|
+
environment,
|
|
902
|
+
systemPreamble,
|
|
903
|
+
executionProfile,
|
|
904
|
+
attachCompatibilityKey,
|
|
905
|
+
agentEnv,
|
|
906
|
+
claudeSettings,
|
|
907
|
+
}) =>
|
|
908
|
+
new ClaudeSession({
|
|
909
|
+
taskId,
|
|
910
|
+
agent,
|
|
911
|
+
environment,
|
|
912
|
+
systemPreamble,
|
|
913
|
+
executionProfile,
|
|
914
|
+
attachCompatibilityKey,
|
|
915
|
+
agentEnv,
|
|
916
|
+
claudeSettings,
|
|
917
|
+
}),
|
|
918
|
+
});
|