@moondeck/station 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +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/meta/_journal.json +223 -0
- package/db/schema.ts +850 -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 +1453 -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 +891 -0
- package/lib/env.ts +195 -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/capture-wire.ts +102 -0
- package/src/cli.ts +2583 -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/event-outbox.ts +185 -0
- package/src/index.ts +2899 -0
- package/src/load-env.ts +97 -0
- package/src/machine-tunnel-route.ts +156 -0
- package/src/main.ts +6349 -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 +5545 -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,334 @@
|
|
|
1
|
+
import { StringDecoder } from "node:string_decoder";
|
|
2
|
+
import { agentOutputSource } from "./output-source";
|
|
3
|
+
/**
|
|
4
|
+
* DurableProcess (ADR-061) — LineProcess's durable twin. Instead of holding
|
|
5
|
+
* the agent CLI's pipes, it talks to the in-container runner
|
|
6
|
+
* (runner/runner.mjs) through two append-only JSONL files on the
|
|
7
|
+
* task-workspace bind mount:
|
|
8
|
+
*
|
|
9
|
+
* writeLine() → append to <sessionDir>/inbox.jsonl
|
|
10
|
+
* onLine() ← poll-tail <sessionDir>/outbox.jsonl from a byte offset
|
|
11
|
+
*
|
|
12
|
+
* The CLI's lifetime is decoupled from this object: dropping it (host
|
|
13
|
+
* restart) leaves the runner and CLI running; `attach()` with the persisted
|
|
14
|
+
* outbox offset resumes exactly where consumption stopped. `close()` is the
|
|
15
|
+
* real teardown — it sends the runner a stop control.
|
|
16
|
+
*
|
|
17
|
+
* Exit is observed via the runner's `{"__uai":"exit"}` meta line, with a
|
|
18
|
+
* stale-heartbeat check as the backstop for a killed container. Meta lines
|
|
19
|
+
* never reach onLine handlers — adapters see the same protocol stream
|
|
20
|
+
* LineProcess gave them.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
appendContainedFile,
|
|
27
|
+
containedModifiedAt,
|
|
28
|
+
NotARegularFileError,
|
|
29
|
+
readContainedFrom,
|
|
30
|
+
WorkspacePathRefusedError,
|
|
31
|
+
} from "../task-environment/workspace-files";
|
|
32
|
+
|
|
33
|
+
import type { ExitHandler, LineHandler } from "./proc";
|
|
34
|
+
|
|
35
|
+
const POLL_MS = 75;
|
|
36
|
+
/** Runner beats every 5 s; 25 s of silence without an exit meta = dead. */
|
|
37
|
+
const HEARTBEAT_STALE_MS = 25_000;
|
|
38
|
+
/** After close(), how long to wait for the exit meta before giving up. */
|
|
39
|
+
const CLOSE_GRACE_MS = 8_000;
|
|
40
|
+
|
|
41
|
+
/** Host-side absolute path of the bundled in-container runner script. */
|
|
42
|
+
export function runnerScriptPath(): string {
|
|
43
|
+
return fileURLToPath(new URL("../../runner/runner.mjs", import.meta.url));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DurableProcessOptions {
|
|
47
|
+
/** Session dir as seen FROM THE HOST (on the workspace bind mount). */
|
|
48
|
+
hostSessionDir: string;
|
|
49
|
+
/** The same directory as the task's workspace root and its components
|
|
50
|
+
* below it: every file in it is reached never through a link the task
|
|
51
|
+
* put there (#cb3j0z). It exists before the runner is launched. */
|
|
52
|
+
sessionFiles: { root: string; parts: readonly string[] };
|
|
53
|
+
/** Settles once that directory has been made, where only the task's
|
|
54
|
+
* runtime can make it (macOS, #cb3j0z): no inbox write, the stop
|
|
55
|
+
* included, is attempted before. Absent: it exists already. */
|
|
56
|
+
ready?: Promise<void>;
|
|
57
|
+
/** Provider-owned detached launch. Omit to attach to an existing runner. */
|
|
58
|
+
launch?: () => Promise<{
|
|
59
|
+
exitCode: number | null;
|
|
60
|
+
stderr: Uint8Array;
|
|
61
|
+
}>;
|
|
62
|
+
/** Resume consumption from this outbox byte offset (attach path). */
|
|
63
|
+
initialOutboxOffset?: number;
|
|
64
|
+
/** Called after each poll batch whose lines were delivered — persist this
|
|
65
|
+
* offset so a host restart resumes here instead of replaying. */
|
|
66
|
+
onOffsetAdvance?: (offset: number) => void;
|
|
67
|
+
/** Called the moment close() is requested (before the runner has actually
|
|
68
|
+
* died) — lets the owner mark the session unattachable immediately, so a
|
|
69
|
+
* re-create during the teardown grace window spawns fresh instead of
|
|
70
|
+
* attaching to a dying runner. */
|
|
71
|
+
onCloseRequested?: () => void;
|
|
72
|
+
/** Mirrors LineProcess: log raw traffic when UAI_DEBUG_AGENTS is set. */
|
|
73
|
+
debugLabel?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface RunnerMeta {
|
|
77
|
+
__uai: string;
|
|
78
|
+
code?: number | null;
|
|
79
|
+
stderrTail?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class DurableProcess {
|
|
83
|
+
readonly durableSessionDir: string;
|
|
84
|
+
private readonly root: string;
|
|
85
|
+
private readonly outbox: readonly string[];
|
|
86
|
+
private readonly inbox: readonly string[];
|
|
87
|
+
private readonly heartbeat: readonly string[];
|
|
88
|
+
|
|
89
|
+
private readonly decoder = new StringDecoder("utf8");
|
|
90
|
+
private lineOffset = 0;
|
|
91
|
+
private readonly lineHandlers = new Set<LineHandler>();
|
|
92
|
+
private readonly exitHandlers = new Set<ExitHandler>();
|
|
93
|
+
|
|
94
|
+
private offset: number;
|
|
95
|
+
private lineBuf = "";
|
|
96
|
+
private stderrBuf = "";
|
|
97
|
+
private closed = false;
|
|
98
|
+
private detached = false;
|
|
99
|
+
private sawSpawnMeta = false;
|
|
100
|
+
private readonly startedAt = Date.now();
|
|
101
|
+
private poller: ReturnType<typeof setInterval> | null = null;
|
|
102
|
+
private closeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
103
|
+
private inboxChain: Promise<void> = Promise.resolve();
|
|
104
|
+
private readonly onOffsetAdvance: ((offset: number) => void) | null;
|
|
105
|
+
private readonly onCloseRequested: (() => void) | null;
|
|
106
|
+
private readonly debug: string | null;
|
|
107
|
+
|
|
108
|
+
constructor(opts: DurableProcessOptions) {
|
|
109
|
+
this.durableSessionDir = opts.hostSessionDir;
|
|
110
|
+
this.root = opts.sessionFiles.root;
|
|
111
|
+
this.outbox = [...opts.sessionFiles.parts, "outbox.jsonl"];
|
|
112
|
+
this.inbox = [...opts.sessionFiles.parts, "inbox.jsonl"];
|
|
113
|
+
this.heartbeat = [...opts.sessionFiles.parts, "heartbeat"];
|
|
114
|
+
this.offset = opts.initialOutboxOffset ?? 0;
|
|
115
|
+
this.lineOffset = this.offset;
|
|
116
|
+
this.onOffsetAdvance = opts.onOffsetAdvance ?? null;
|
|
117
|
+
this.onCloseRequested = opts.onCloseRequested ?? null;
|
|
118
|
+
if (opts.ready) {
|
|
119
|
+
this.inboxChain = opts.ready.then(
|
|
120
|
+
() => undefined,
|
|
121
|
+
() => undefined,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
this.debug =
|
|
125
|
+
opts.debugLabel && process.env.UAI_DEBUG_AGENTS ? opts.debugLabel : null;
|
|
126
|
+
|
|
127
|
+
if (opts.launch) {
|
|
128
|
+
void opts.launch().then(
|
|
129
|
+
(result) => {
|
|
130
|
+
const stderr = Buffer.from(result.stderr).toString("utf8");
|
|
131
|
+
if (stderr) this.stderrBuf = (this.stderrBuf + stderr).slice(-8192);
|
|
132
|
+
if (result.exitCode !== 0) this.finish(null);
|
|
133
|
+
},
|
|
134
|
+
(error: unknown) => {
|
|
135
|
+
this.stderrBuf = (
|
|
136
|
+
this.stderrBuf +
|
|
137
|
+
(error instanceof Error ? error.message : String(error))
|
|
138
|
+
).slice(-8192);
|
|
139
|
+
this.finish(null);
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
this.sawSpawnMeta = true; // attach: the runner pre-exists
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.poller = setInterval(() => this.poll(), POLL_MS);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private log(msg: string): void {
|
|
150
|
+
console.error(`[uai-agent ${this.debug}] ${msg}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---- outbox tail ---------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
private poll(): void {
|
|
156
|
+
if (this.closed || this.detached) return;
|
|
157
|
+
const before = this.offset;
|
|
158
|
+
try {
|
|
159
|
+
for (;;) {
|
|
160
|
+
const read = readContainedFrom(this.root, this.outbox, this.offset, 256 * 1024);
|
|
161
|
+
if (read === null) break; // outbox not created yet
|
|
162
|
+
if (read.bytes.length === 0) break;
|
|
163
|
+
this.offset += read.bytes.length;
|
|
164
|
+
this.lineBuf += this.decoder.write(read.bytes);
|
|
165
|
+
this.drainLines();
|
|
166
|
+
if (this.offset >= read.size) break;
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error instanceof WorkspacePathRefusedError || error instanceof NotARegularFileError) {
|
|
170
|
+
// A link, or anything but a regular file, where the outbox is: the
|
|
171
|
+
// task broke its own session, and it ends (#cb3j0z).
|
|
172
|
+
this.finish(null);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Anything else may pass: the heartbeat decides, below.
|
|
176
|
+
}
|
|
177
|
+
if (this.offset !== before && this.onOffsetAdvance && !this.closed) {
|
|
178
|
+
try {
|
|
179
|
+
this.onOffsetAdvance(this.lineOffset);
|
|
180
|
+
} catch {
|
|
181
|
+
// Persistence hiccup — the next batch retries with a larger offset.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
this.checkLiveness();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private drainLines(): void {
|
|
188
|
+
let nl: number;
|
|
189
|
+
while ((nl = this.lineBuf.indexOf("\n")) >= 0) {
|
|
190
|
+
const rawLine = this.lineBuf.slice(0, nl);
|
|
191
|
+
this.lineOffset += Buffer.byteLength(rawLine) + 1;
|
|
192
|
+
const line = rawLine.trim();
|
|
193
|
+
this.lineBuf = this.lineBuf.slice(nl + 1);
|
|
194
|
+
if (line.length === 0) continue;
|
|
195
|
+
if (line.startsWith('{"__uai"')) {
|
|
196
|
+
this.handleMeta(line);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (this.debug) this.log(`<- ${line.slice(0, 1000)}`);
|
|
200
|
+
for (const h of this.lineHandlers) {
|
|
201
|
+
try {
|
|
202
|
+
agentOutputSource.run(this.durableSessionDir + ":" + this.lineOffset, () => h(line));
|
|
203
|
+
} catch {
|
|
204
|
+
// A broken handler must not wedge the tail loop.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private handleMeta(line: string): void {
|
|
211
|
+
let meta: RunnerMeta;
|
|
212
|
+
try {
|
|
213
|
+
meta = JSON.parse(line) as RunnerMeta;
|
|
214
|
+
} catch {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (meta.__uai === "spawn") {
|
|
218
|
+
this.sawSpawnMeta = true;
|
|
219
|
+
} else if (meta.__uai === "exit") {
|
|
220
|
+
if (typeof meta.stderrTail === "string" && meta.stderrTail) {
|
|
221
|
+
this.stderrBuf = meta.stderrTail.slice(-8192);
|
|
222
|
+
}
|
|
223
|
+
if (this.debug) this.log(`exit meta: code ${meta.code ?? null}`);
|
|
224
|
+
this.finish(typeof meta.code === "number" ? meta.code : null);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Backstop: no exit meta, but the runner stopped beating → it's gone. */
|
|
229
|
+
private checkLiveness(): void {
|
|
230
|
+
if (!this.sawSpawnMeta) {
|
|
231
|
+
// Runner never wrote anything; give the spawn a grace window.
|
|
232
|
+
if (Date.now() - this.startedAt > HEARTBEAT_STALE_MS) this.finish(null);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
let beat: number | null = null;
|
|
236
|
+
try {
|
|
237
|
+
beat = containedModifiedAt(this.root, this.heartbeat);
|
|
238
|
+
} catch {
|
|
239
|
+
beat = null;
|
|
240
|
+
}
|
|
241
|
+
if (beat !== null) {
|
|
242
|
+
if (Date.now() - beat > HEARTBEAT_STALE_MS) this.finish(null);
|
|
243
|
+
} else if (Date.now() - this.startedAt > HEARTBEAT_STALE_MS) {
|
|
244
|
+
this.finish(null);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private finish(code: number | null): void {
|
|
249
|
+
if (this.closed) return;
|
|
250
|
+
this.closed = true;
|
|
251
|
+
if (this.poller) clearInterval(this.poller);
|
|
252
|
+
if (this.closeTimer) clearTimeout(this.closeTimer);
|
|
253
|
+
for (const h of this.exitHandlers) h(code);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---- LineProcess-compatible surface ---------------------------------------
|
|
257
|
+
|
|
258
|
+
onLine(handler: LineHandler): void {
|
|
259
|
+
this.lineHandlers.add(handler);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
onExit(handler: ExitHandler): void {
|
|
263
|
+
this.exitHandlers.add(handler);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Serialise `value` as one JSONL line appended to the runner's inbox.
|
|
268
|
+
* A runner-filtered metadata line immediately before it supplies the local
|
|
269
|
+
* timestamp without changing the protocol frame forwarded to the CLI. Old
|
|
270
|
+
* runners already ignore unknown `__uai` lines, so attached sessions remain
|
|
271
|
+
* wire-compatible across the host update.
|
|
272
|
+
*
|
|
273
|
+
* `__uai` MUST stay the first key of the metadata object. Both runners filter
|
|
274
|
+
* with `line.startsWith('{"__uai"')` — a byte prefix, not a parse — so
|
|
275
|
+
* serialising the keys in any other order emits `{"ts":…`, which the runner
|
|
276
|
+
* then forwards to the CLI's stdin as a user turn. No type in this file
|
|
277
|
+
* expresses that, and a `toEqual` assertion cannot see it; the raw-prefix
|
|
278
|
+
* check in `durable-proc.test.ts` is what pins it.
|
|
279
|
+
*/
|
|
280
|
+
writeLine(value: unknown): void {
|
|
281
|
+
if (this.closed || this.detached) return;
|
|
282
|
+
const json = JSON.stringify(value);
|
|
283
|
+
const inputMeta = JSON.stringify({ __uai: "input", ts: Date.now() });
|
|
284
|
+
if (this.debug) this.log(`-> ${json.slice(0, 1000)}`);
|
|
285
|
+
// Chain appends so concurrent writes can't interleave bytes.
|
|
286
|
+
this.inboxChain = this.inboxChain
|
|
287
|
+
.then(() => appendContainedFile(this.root, this.inbox, Buffer.from(`${inputMeta}\n${json}\n`)))
|
|
288
|
+
.catch(() => {
|
|
289
|
+
// Disk error — liveness checks will surface a dead session.
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
get stderrTail(): string {
|
|
294
|
+
return this.stderrBuf;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
get isClosed(): boolean {
|
|
298
|
+
return this.closed;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Consumed-outbox byte offset — persist this to survive host restarts. */
|
|
302
|
+
get outboxOffset(): number {
|
|
303
|
+
return this.lineOffset;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Stop consuming WITHOUT touching the runner — the host-restart path in
|
|
308
|
+
* miniature (a replaced consumer, a shutting-down host). The CLI lives on;
|
|
309
|
+
* a later attach() resumes from the persisted offset.
|
|
310
|
+
*/
|
|
311
|
+
detach(): void {
|
|
312
|
+
if (this.detached || this.closed) return;
|
|
313
|
+
this.detached = true;
|
|
314
|
+
if (this.poller) clearInterval(this.poller);
|
|
315
|
+
if (this.closeTimer) clearTimeout(this.closeTimer);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Real teardown: ask the runner to stop its CLI, then observe the exit. */
|
|
319
|
+
async close(): Promise<void> {
|
|
320
|
+
if (this.closed || this.detached) return;
|
|
321
|
+
try {
|
|
322
|
+
this.onCloseRequested?.();
|
|
323
|
+
} catch {
|
|
324
|
+
// Bookkeeping only — never blocks the teardown itself.
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
await this.inboxChain;
|
|
328
|
+
await appendContainedFile(this.root, this.inbox, Buffer.from('{"__uai":"stop"}\n'));
|
|
329
|
+
} catch {
|
|
330
|
+
// Inbox unwritable — fall through to the grace timer.
|
|
331
|
+
}
|
|
332
|
+
this.closeTimer = setTimeout(() => this.finish(null), CLOSE_GRACE_MS);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* realAgentFactory — builds a real AgentSession per roster agent, resolving
|
|
3
|
+
* the adapter from the plug-in registry by `agent.kind` (ADR-021).
|
|
4
|
+
*
|
|
5
|
+
* Importing this module pulls in `./claude` and `./codex` purely for their
|
|
6
|
+
* side effect: each calls `register()` at load, populating the registry.
|
|
7
|
+
* Adding a kind is then "drop a module + import it for its side effect" — no
|
|
8
|
+
* change here. Unknown kinds fail loudly so a typo in a roster surfaces as an
|
|
9
|
+
* error rather than silently falling back to the wrong CLI.
|
|
10
|
+
*
|
|
11
|
+
* Adapters run their CLI through the task environment's provider-owned
|
|
12
|
+
* session surface; the agent's `model` (if set) is passed through. Swapping
|
|
13
|
+
* mock ⇄ real is changing which factory the orchestrator is constructed with
|
|
14
|
+
* — see `getOrchestrator`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Side-effect imports: register the built-in adapters with the registry.
|
|
18
|
+
import "./claude";
|
|
19
|
+
import "./codex";
|
|
20
|
+
import "./kimi";
|
|
21
|
+
import "./grok";
|
|
22
|
+
import "./cursor";
|
|
23
|
+
import "./opencode";
|
|
24
|
+
import "./uai";
|
|
25
|
+
|
|
26
|
+
import { agentClisReady } from "../standard-image";
|
|
27
|
+
import { factoryFor, supportsExecutionProfile } from "./registry";
|
|
28
|
+
import type { AgentSession, AgentSessionFactory } from "./types";
|
|
29
|
+
|
|
30
|
+
export const realAgentFactory: AgentSessionFactory = {
|
|
31
|
+
create: async (args): Promise<AgentSession> => {
|
|
32
|
+
const factory = factoryFor(args.agent.kind);
|
|
33
|
+
if (!factory) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`no agent adapter registered for kind "${args.agent.kind}"`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (
|
|
39
|
+
args.executionProfile &&
|
|
40
|
+
!supportsExecutionProfile(args.agent.kind, args.executionProfile)
|
|
41
|
+
) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`agent adapter "${args.agent.kind}" cannot enforce execution profile "${args.executionProfile}"`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
// Don't spawn a CLI until the shared-volume agent CLIs are reconciled:
|
|
47
|
+
// at boot the CLI auto-upgrade briefly removes then reinstalls codex/claude,
|
|
48
|
+
// and a resume that races that window dies with "No codex executable found
|
|
49
|
+
// for nodejs X". Post-boot this is already resolved (instant).
|
|
50
|
+
// Fail closed: the maintenance commands have their own process timeouts
|
|
51
|
+
// and settle this latch even on failure. A second timeout here would let a
|
|
52
|
+
// CLI spawn enter the shared volume while boot reconciliation still owns
|
|
53
|
+
// its remove/reinstall window.
|
|
54
|
+
await agentClisReady;
|
|
55
|
+
return factory.create(args);
|
|
56
|
+
},
|
|
57
|
+
};
|