@ours.network/fleet 1.2.0-nightly.3 → 1.2.0-nightly.30
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/README.md +158 -28
- package/dist/account-origin.d.ts +2 -0
- package/dist/account-origin.js +6 -0
- package/dist/agent-ours/bridge.d.ts +3 -0
- package/dist/agent-ours/bridge.js +178 -0
- package/dist/agent-ours/controller.d.ts +14 -0
- package/dist/agent-ours/controller.js +60 -0
- package/dist/agent-ours/correspondence.d.ts +10 -0
- package/dist/agent-ours/correspondence.js +1 -0
- package/dist/agent-ours/file-rpc.d.ts +23 -0
- package/dist/agent-ours/file-rpc.js +123 -0
- package/dist/agent-ours/harness.d.ts +14 -0
- package/dist/agent-ours/harness.js +72 -0
- package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
- package/dist/agent-ours/mcp-endpoint.js +111 -0
- package/dist/agent-ours/runtime.d.ts +59 -0
- package/dist/agent-ours/runtime.js +283 -0
- package/dist/agent-ours/service.d.ts +23 -0
- package/dist/agent-ours/service.js +372 -0
- package/dist/agent-ours/state.d.ts +45 -0
- package/dist/agent-ours/state.js +95 -0
- package/dist/agent-ours/wire.d.ts +14 -0
- package/dist/agent-ours/wire.js +55 -0
- package/dist/application/conversation-history.d.ts +25 -0
- package/dist/application/conversation-history.js +189 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +2 -2
- package/dist/application/legacy-supervisor-identity.d.ts +7 -0
- package/dist/application/legacy-supervisor-identity.js +43 -0
- package/dist/application/model-catalog.d.ts +1 -1
- package/dist/application/model-catalog.js +5 -2
- package/dist/application/preset-provenance.d.ts +19 -0
- package/dist/application/preset-provenance.js +59 -0
- package/dist/application/room-layout-definitions.d.ts +45 -0
- package/dist/application/room-layout-definitions.js +130 -0
- package/dist/application/session-control.d.ts +18 -0
- package/dist/application/session-control.js +54 -3
- package/dist/application/supervisor-ours-tools.d.ts +159 -0
- package/dist/application/supervisor-ours-tools.js +113 -0
- package/dist/application/task-layouts.d.ts +91 -0
- package/dist/application/task-layouts.js +249 -0
- package/dist/application/task-room-service.d.ts +99 -3
- package/dist/application/task-room-service.js +296 -31
- package/dist/atomic-file.d.ts +5 -0
- package/dist/atomic-file.js +62 -1
- package/dist/briefing.js +10 -59
- package/dist/build-info.json +6 -4
- package/dist/capabilities.d.ts +3 -1
- package/dist/capabilities.js +4 -0
- package/dist/cli.js +109 -3
- package/dist/client-profile.d.ts +5 -16
- package/dist/client-profile.js +4 -112
- package/dist/config.d.ts +4 -0
- package/dist/config.js +7 -0
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +5 -16
- package/dist/daemon-recovery.d.ts +1 -8
- package/dist/daemon-recovery.js +8 -113
- package/dist/docs.d.ts +3 -3
- package/dist/docs.js +43 -23
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +46 -59
- package/dist/erased-resources.d.ts +9 -0
- package/dist/erased-resources.js +87 -0
- package/dist/fleet-command-audit.d.ts +1 -0
- package/dist/fleet-command-audit.js +12 -7
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +4 -2
- package/dist/harness/codex-app-server-proxy.d.ts +4 -1
- package/dist/harness/codex-app-server-proxy.js +41 -5
- package/dist/harness/codex-session.js +9 -2
- package/dist/harness/codex.d.ts +3 -2
- package/dist/harness/codex.js +4 -3
- package/dist/harness/hermes-session.js +7 -1
- package/dist/init-wizard.js +20 -9
- package/dist/isolation/policy.js +2 -0
- package/dist/isolation/types.d.ts +5 -0
- package/dist/monitor.d.ts +1 -34
- package/dist/monitor.js +24 -151
- package/dist/notifications/fleet-producer.d.ts +18 -0
- package/dist/notifications/fleet-producer.js +129 -0
- package/dist/notifications/outbox.d.ts +31 -0
- package/dist/notifications/outbox.js +104 -0
- package/dist/owner-channel/channel.d.ts +3 -0
- package/dist/owner-channel/channel.js +34 -6
- package/dist/owner-channel/commands.d.ts +4 -0
- package/dist/owner-channel/commands.js +8 -0
- package/dist/owner-channel/lifecycle-outbox.js +4 -3
- package/dist/owner-channel/ours-client.js +2 -17
- package/dist/preset-migration.js +15 -0
- package/dist/rooms-tasks/add-member.d.ts +48 -0
- package/dist/rooms-tasks/add-member.js +83 -0
- package/dist/rooms-tasks/archived-absence.d.ts +8 -0
- package/dist/rooms-tasks/archived-absence.js +48 -0
- package/dist/rooms-tasks/cli.js +34 -5
- package/dist/rooms-tasks/close.d.ts +5 -2
- package/dist/rooms-tasks/close.js +208 -28
- package/dist/rooms-tasks/config.js +3 -1
- package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
- package/dist/rooms-tasks/cowork-adapter.js +11 -117
- package/dist/rooms-tasks/cowork-http.d.ts +3 -0
- package/dist/rooms-tasks/cowork-http.js +70 -0
- package/dist/rooms-tasks/deletion.d.ts +1 -1
- package/dist/rooms-tasks/deletion.js +74 -5
- package/dist/rooms-tasks/erasure.d.ts +6 -0
- package/dist/rooms-tasks/erasure.js +169 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -1
- package/dist/rooms-tasks/external-worker.js +3 -4
- package/dist/rooms-tasks/layout-binding-client.d.ts +11 -0
- package/dist/rooms-tasks/layout-binding-client.js +56 -0
- package/dist/rooms-tasks/layout-binding-grants.d.ts +21 -0
- package/dist/rooms-tasks/layout-binding-grants.js +128 -0
- package/dist/rooms-tasks/layout-cli.d.ts +4 -0
- package/dist/rooms-tasks/layout-cli.js +107 -0
- package/dist/rooms-tasks/layout-config.d.ts +11 -0
- package/dist/rooms-tasks/layout-config.js +89 -0
- package/dist/rooms-tasks/layout-control.d.ts +21 -0
- package/dist/rooms-tasks/layout-control.js +52 -0
- package/dist/rooms-tasks/layout-service.d.ts +42 -0
- package/dist/rooms-tasks/layout-service.js +185 -0
- package/dist/rooms-tasks/layout.d.ts +114 -0
- package/dist/rooms-tasks/layout.js +250 -0
- package/dist/rooms-tasks/live-readiness.d.ts +18 -0
- package/dist/rooms-tasks/live-readiness.js +85 -0
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +2 -0
- package/dist/rooms-tasks/provision.d.ts +14 -0
- package/dist/rooms-tasks/provision.js +182 -48
- package/dist/rooms-tasks/room-state.d.ts +4 -2
- package/dist/rooms-tasks/room-state.js +53 -8
- package/dist/rooms-tasks/task-state.d.ts +42 -7
- package/dist/rooms-tasks/task-state.js +153 -23
- package/dist/rooms-tasks/types.d.ts +28 -0
- package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
- package/dist/rooms-tasks/workspace-artifacts.js +93 -0
- package/dist/rooms-tasks/workspace.d.ts +26 -0
- package/dist/rooms-tasks/workspace.js +195 -0
- package/dist/runner.d.ts +23 -3
- package/dist/runner.js +581 -494
- package/dist/session/acp.d.ts +3 -0
- package/dist/session/acp.js +14 -0
- package/dist/session/arbiter.d.ts +3 -0
- package/dist/session/arbiter.js +20 -2
- package/dist/session/codex-app-server-transport.js +1 -1
- package/dist/session/codex-app-server.d.ts +3 -0
- package/dist/session/codex-app-server.js +25 -1
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +37 -2
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-types.d.ts +5 -3
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +3 -0
- package/dist/socket-path.d.ts +2 -0
- package/dist/socket-path.js +22 -0
- package/dist/spawn.js +19 -16
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -0
- package/dist/subscriptions/cli.d.ts +78 -0
- package/dist/subscriptions/cli.js +186 -0
- package/dist/subscriptions/launch.d.ts +37 -0
- package/dist/subscriptions/launch.js +93 -0
- package/dist/subscriptions/login.d.ts +75 -0
- package/dist/subscriptions/login.js +182 -0
- package/dist/subscriptions/observed.d.ts +7 -0
- package/dist/subscriptions/observed.js +44 -0
- package/dist/subscriptions/probe.d.ts +10 -0
- package/dist/subscriptions/probe.js +73 -0
- package/dist/subscriptions/service.d.ts +102 -0
- package/dist/subscriptions/service.js +364 -0
- package/dist/subscriptions/store.d.ts +71 -0
- package/dist/subscriptions/store.js +140 -0
- package/dist/supervisor/launchd.js +2 -1
- package/dist/supervisor/systemd.js +9 -23
- package/dist/temp-idle.d.ts +17 -0
- package/dist/temp-idle.js +42 -0
- package/dist/temp-lifecycle.d.ts +6 -1
- package/dist/temp-lifecycle.js +29 -13
- package/dist/temp-supervisor-recovery.d.ts +3 -0
- package/dist/temp-supervisor-recovery.js +46 -0
- package/dist/watchdog/briefing.js +3 -16
- package/dist/watchdog/run.js +19 -5
- package/dist/web/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +28 -2
- package/dist/web/auth.js +81 -8
- package/dist/web/control.d.ts +3 -2
- package/dist/web/control.js +11 -7
- package/dist/web/fleet-config-service.d.ts +4 -1
- package/dist/web/fleet-config-service.js +46 -15
- package/dist/web/prefix-gateway.d.ts +23 -0
- package/dist/web/prefix-gateway.js +156 -0
- package/dist/web/runtime.d.ts +4 -0
- package/dist/web/runtime.js +90 -19
- package/dist/web/server.d.ts +11 -0
- package/dist/web/server.js +445 -31
- package/dist/web/service.d.ts +6 -2
- package/dist/web/service.js +22 -4
- package/dist/web/workspace-devices.d.ts +29 -0
- package/dist/web/workspace-devices.js +84 -0
- package/dist/web/workspace-tunnel.d.ts +1 -0
- package/dist/web/workspace-tunnel.js +32 -0
- package/dist/workspace-enrollment.d.ts +25 -0
- package/dist/workspace-enrollment.js +112 -0
- package/docs/room-layouts.md +227 -0
- package/docs/task-workspaces.md +130 -0
- package/examples/room-layouts/fresh.yaml +27 -0
- package/examples/room-layouts/scoped.yaml +21 -0
- package/examples/room-layouts/shared.yaml +19 -0
- package/package.json +14 -17
- package/presets/README.md +78 -0
- package/presets/brain-catalog.json +8 -2
- package/presets/fleet/agent_templates/Engineer.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
- package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +22 -0
- package/presets/fleet/roles/Engineer.yaml +40 -0
- package/presets/fleet/room_layouts/engineering.yaml +25 -0
- package/presets/fleet/room_layouts/pair.yaml +24 -0
- package/presets/fleet/room_layouts/single.yaml +17 -0
- package/presets/fleet/room_layouts/team.yaml +30 -0
- package/presets/fleet/room_templates/engineering.yaml +19 -0
- package/presets/manifest.json +2 -2
- package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
- package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
- package/dist/web-app/icons/ours-fleet.svg +0 -4
- package/dist/web-app/index.html +0 -17
- package/dist/web-app/manifest.webmanifest +0 -15
- package/dist/web-app/offline.html +0 -18
- package/dist/web-app/sw.js +0 -51
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ResolvedRole } from '../config.js';
|
|
2
|
+
import { AgentOursRuntime } from './runtime.js';
|
|
3
|
+
export declare const privateRuntimeRoot: () => string;
|
|
4
|
+
export interface ManagedAgentService {
|
|
5
|
+
runtime: AgentOursRuntime;
|
|
6
|
+
descriptor: string;
|
|
7
|
+
privatePaths: string[];
|
|
8
|
+
close(terminal: boolean): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/** Only trusted launch orchestration may write this descriptor, never the child. */
|
|
11
|
+
export declare function storeRoomSecret(role: ResolvedRole): void;
|
|
12
|
+
export declare function storeTemporaryLaunch(role: ResolvedRole, action: string): void;
|
|
13
|
+
export declare function prepareManagedAgent(role: ResolvedRole, stateDir: string, temporary: boolean): Promise<ManagedAgentService>;
|
|
14
|
+
/** Explicit trusted inventory migration: read and pin existing permanent CID, never bind. */
|
|
15
|
+
export declare function preparePermanentAssignment(role: ResolvedRole): Promise<'verified' | 'unverified'>;
|
|
16
|
+
export declare function readRoomReadiness(roomCid: string, name: string): {
|
|
17
|
+
room: string;
|
|
18
|
+
invite: string;
|
|
19
|
+
cid: string;
|
|
20
|
+
generation: number;
|
|
21
|
+
} | undefined;
|
|
22
|
+
/** Explicit logical-instance termination, independent of harness/bridge transport. */
|
|
23
|
+
export declare function releaseManagedAgent(role: ResolvedRole): Promise<void>;
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { socketPath as privateSocketPath } from '../socket-path.js';
|
|
2
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { attachOursClient } from '@ours.network/sdk/client';
|
|
6
|
+
import { ApplicationIdentityStore } from '@ours.network/mcp/application-identities';
|
|
7
|
+
import { readClientProfile } from '../client-profile.js';
|
|
8
|
+
import { stateRoot } from '../paths.js';
|
|
9
|
+
import { createCoworkAdapter } from '../rooms-tasks/cowork-adapter.js';
|
|
10
|
+
import { RuntimeController } from './controller.js';
|
|
11
|
+
import { atomicPrivateWrite, binderKey } from './state.js';
|
|
12
|
+
import { AgentOursRuntime } from './runtime.js';
|
|
13
|
+
import { startMcpEndpoint } from './mcp-endpoint.js';
|
|
14
|
+
export const privateRuntimeRoot = () => join(stateRoot(), 'private-ours');
|
|
15
|
+
function roomSecretPath(role) {
|
|
16
|
+
const startup = role.roomMemberStartup;
|
|
17
|
+
return join(privateRuntimeRoot(), 'room-inputs', binderKey(startup.room_identity_cid, JSON.stringify([role.name, startup.invite_id])) + '.json');
|
|
18
|
+
}
|
|
19
|
+
function matchesRoomSecret(secret, role) {
|
|
20
|
+
const startup = role.roomMemberStartup;
|
|
21
|
+
return secret.room_id === startup.room_id && secret.room_identity_cid === startup.room_identity_cid
|
|
22
|
+
&& secret.invite_id === startup.invite_id && secret.identity_name === role.identity
|
|
23
|
+
&& secret.role === startup.role;
|
|
24
|
+
}
|
|
25
|
+
/** Only trusted launch orchestration may write this descriptor, never the child. */
|
|
26
|
+
export function storeRoomSecret(role) {
|
|
27
|
+
const startup = role.roomMemberStartup;
|
|
28
|
+
if (!startup?.invite)
|
|
29
|
+
return;
|
|
30
|
+
if (startup.identity_name !== role.identity)
|
|
31
|
+
throw Error('ROOM_SECRET_MISMATCH');
|
|
32
|
+
const root = join(privateRuntimeRoot(), 'room-inputs');
|
|
33
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
34
|
+
// Failed attempts retain private evidence. A new invite must never overwrite
|
|
35
|
+
// or consume a previous attempt's descriptor.
|
|
36
|
+
const path = roomSecretPath(role);
|
|
37
|
+
if (existsSync(path)) {
|
|
38
|
+
const old = JSON.parse(readFileSync(path, 'utf8'));
|
|
39
|
+
if (!matchesRoomSecret(old, role) || old.invite !== startup.invite)
|
|
40
|
+
throw Error('ROOM_SECRET_COLLISION');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
atomicPrivateWrite(path, startup);
|
|
44
|
+
}
|
|
45
|
+
export function storeTemporaryLaunch(role, action) {
|
|
46
|
+
const root = join(privateRuntimeRoot(), 'launches');
|
|
47
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
48
|
+
atomicPrivateWrite(join(root, binderKey('temporary', role.name) + '.json'), {
|
|
49
|
+
role: role.name,
|
|
50
|
+
identity: role.identity,
|
|
51
|
+
action,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
55
|
+
if (role.owner_channel?.identity === role.identity)
|
|
56
|
+
throw Error('OWNER_AGENT_IDENTITY_COLLISION');
|
|
57
|
+
const profile = readClientProfile({ ...process.env, ...role.env });
|
|
58
|
+
if (!profile)
|
|
59
|
+
throw Error('MANAGED_OURS_REQUIRES_EXPLICIT_DAEMON_PROFILE');
|
|
60
|
+
const root = privateRuntimeRoot(), key = binderKey(profile.expectedInstanceId, role.identity), dir = join(root, key);
|
|
61
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
62
|
+
const instancePath = join(dir, 'instance.json');
|
|
63
|
+
const launch = temporary
|
|
64
|
+
? JSON.parse(readFileSync(join(root, 'launches', binderKey('temporary', role.name) + '.json'), 'utf8'))
|
|
65
|
+
: undefined;
|
|
66
|
+
if (launch &&
|
|
67
|
+
(launch.role !== role.name ||
|
|
68
|
+
launch.identity !== role.identity ||
|
|
69
|
+
typeof launch.action !== 'string'))
|
|
70
|
+
throw Error('INVALID_TEMP_LAUNCH');
|
|
71
|
+
let instance;
|
|
72
|
+
if (existsSync(instancePath)) {
|
|
73
|
+
const old = JSON.parse(readFileSync(instancePath, 'utf8'));
|
|
74
|
+
if (old.role !== role.name || old.temporary !== temporary)
|
|
75
|
+
throw Error('IDENTITY_ASSIGNED_TO_OTHER_ROLE');
|
|
76
|
+
instance = old.instance;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
instance = randomUUID();
|
|
80
|
+
try {
|
|
81
|
+
writeFileSync(instancePath, JSON.stringify({ instance, role: role.name, temporary }), {
|
|
82
|
+
flag: 'wx',
|
|
83
|
+
mode: 0o600,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error.code !== 'EEXIST')
|
|
88
|
+
throw error;
|
|
89
|
+
const old = JSON.parse(readFileSync(instancePath, 'utf8'));
|
|
90
|
+
if (old.role !== role.name || old.temporary !== temporary)
|
|
91
|
+
throw Error('IDENTITY_ASSIGNED_TO_OTHER_ROLE');
|
|
92
|
+
instance = old.instance;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const controller = await RuntimeController.acquire(root, profile.expectedInstanceId, role.identity, instance);
|
|
96
|
+
let closeClient;
|
|
97
|
+
let partialEndpoint;
|
|
98
|
+
try {
|
|
99
|
+
let prior = controller.journal.read();
|
|
100
|
+
if (prior?.phase === 'RELEASED') {
|
|
101
|
+
if (temporary && launch.action === prior.action)
|
|
102
|
+
throw Error('TERMINAL_TEMP_REQUIRES_NEW_LAUNCH');
|
|
103
|
+
instance = controller.successor();
|
|
104
|
+
atomicPrivateWrite(instancePath, { instance, role: role.name, temporary });
|
|
105
|
+
if (temporary && existsSync(join(dir, 'identity-pin.json')))
|
|
106
|
+
unlinkSync(join(dir, 'identity-pin.json'));
|
|
107
|
+
prior = undefined;
|
|
108
|
+
}
|
|
109
|
+
const generation = prior ? controller.resumeGeneration() : 1;
|
|
110
|
+
const tokenPath = join(dir, 'owner.json');
|
|
111
|
+
let token;
|
|
112
|
+
if (existsSync(tokenPath)) {
|
|
113
|
+
const owner = JSON.parse(readFileSync(tokenPath, 'utf8'));
|
|
114
|
+
if (owner.instance !== instance || typeof owner.token !== 'string')
|
|
115
|
+
throw Error('OWNER_RECORD_MISMATCH');
|
|
116
|
+
token = owner.token;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
if (prior)
|
|
120
|
+
throw Error('OWNER_RECORD_MISSING');
|
|
121
|
+
token = randomBytes(32).toString('hex');
|
|
122
|
+
atomicPrivateWrite(tokenPath, { instance, token });
|
|
123
|
+
}
|
|
124
|
+
const client = await attachOursClient({
|
|
125
|
+
endpoint: profile.endpoint,
|
|
126
|
+
expectedInstanceId: profile.expectedInstanceId,
|
|
127
|
+
credentialPath: profile.credentialPath,
|
|
128
|
+
sessionMode: 'external',
|
|
129
|
+
leaseToken: token,
|
|
130
|
+
requiredCapabilities: ['external-sessions-v1', 'root-first-identities-v1'],
|
|
131
|
+
});
|
|
132
|
+
closeClient = () => client.close();
|
|
133
|
+
const pinPath = join(dir, 'identity-pin.json');
|
|
134
|
+
const pin = existsSync(pinPath) ? JSON.parse(readFileSync(pinPath, 'utf8')) : undefined;
|
|
135
|
+
if (pin &&
|
|
136
|
+
(pin.daemon !== profile.expectedInstanceId ||
|
|
137
|
+
pin.name !== role.identity ||
|
|
138
|
+
typeof pin.cid !== 'string'))
|
|
139
|
+
throw Error('INVALID_IDENTITY_PIN');
|
|
140
|
+
const runtime = new AgentOursRuntime({
|
|
141
|
+
instance,
|
|
142
|
+
generation,
|
|
143
|
+
daemon: profile.expectedInstanceId,
|
|
144
|
+
name: role.identity,
|
|
145
|
+
lifetime: temporary ? 'temporary' : 'permanent',
|
|
146
|
+
action: launch?.action ?? prior?.action ?? randomUUID(),
|
|
147
|
+
expectedCid: pin?.cid,
|
|
148
|
+
allowCreate: true,
|
|
149
|
+
bio: role.bio ?? '',
|
|
150
|
+
}, {
|
|
151
|
+
client,
|
|
152
|
+
journal: controller.journal,
|
|
153
|
+
assertFence: controller.assertFence,
|
|
154
|
+
now: Date.now,
|
|
155
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
156
|
+
});
|
|
157
|
+
let room;
|
|
158
|
+
if (role.roomMemberStartup) {
|
|
159
|
+
const startup = role.roomMemberStartup;
|
|
160
|
+
const legacyPath = join(root, 'room-inputs', binderKey(startup.room_identity_cid, role.name) + '.json');
|
|
161
|
+
const currentPath = roomSecretPath(role);
|
|
162
|
+
const path = existsSync(currentPath) ? currentPath : legacyPath;
|
|
163
|
+
const cowork = createCoworkAdapter();
|
|
164
|
+
room = {
|
|
165
|
+
id: startup.room_id,
|
|
166
|
+
cid: startup.room_identity_cid,
|
|
167
|
+
seat: startup.invite_id,
|
|
168
|
+
action: startup.invite_id,
|
|
169
|
+
redeem: async (attached) => {
|
|
170
|
+
const secret = JSON.parse(readFileSync(path, 'utf8'));
|
|
171
|
+
if (!matchesRoomSecret(secret, role))
|
|
172
|
+
throw Error('ROOM_SECRET_MISMATCH');
|
|
173
|
+
return attached.addContact({ invite: secret.invite });
|
|
174
|
+
},
|
|
175
|
+
observe: async (agentCid) => {
|
|
176
|
+
const [contacts, observed] = await Promise.all([
|
|
177
|
+
client.listContacts(),
|
|
178
|
+
cowork.getRoom(startup.room_id),
|
|
179
|
+
]);
|
|
180
|
+
if (!observed || observed.identity_cid !== startup.room_identity_cid)
|
|
181
|
+
return 'mismatch';
|
|
182
|
+
const seat = observed.seats.find((s) => s.invite_id === startup.invite_id);
|
|
183
|
+
if (seat && (seat.identity_cid !== agentCid || seat.role !== startup.role))
|
|
184
|
+
return 'mismatch';
|
|
185
|
+
return seat?.seat_state === 'active' &&
|
|
186
|
+
contacts.contacts.some((c) => c.container_id === startup.room_identity_cid)
|
|
187
|
+
? 'established'
|
|
188
|
+
: 'pending';
|
|
189
|
+
},
|
|
190
|
+
discardSecret: async () => {
|
|
191
|
+
if (existsSync(path))
|
|
192
|
+
unlinkSync(path);
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
await runtime.prepare(room);
|
|
197
|
+
if (role.roomMemberStartup) {
|
|
198
|
+
const startup = role.roomMemberStartup;
|
|
199
|
+
atomicPrivateWrite(join(root, 'room-inputs', binderKey(startup.room_identity_cid, role.name) + '.ready.json'), { room: startup.room_id, invite: startup.invite_id, cid: runtime.snapshot.cid, generation });
|
|
200
|
+
}
|
|
201
|
+
if (!pin)
|
|
202
|
+
atomicPrivateWrite(pinPath, {
|
|
203
|
+
daemon: profile.expectedInstanceId,
|
|
204
|
+
name: role.identity,
|
|
205
|
+
cid: runtime.snapshot.cid,
|
|
206
|
+
});
|
|
207
|
+
const identities = new ApplicationIdentityStore({ instanceId: profile.expectedInstanceId }, { path: join(dir, 'application.json') });
|
|
208
|
+
await identities.add(role.identity);
|
|
209
|
+
const bridgeDir = join(stateDir, '.ours-bridge');
|
|
210
|
+
mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
|
|
211
|
+
const capability = randomBytes(32).toString('hex'), socket = privateSocketPath(join(bridgeDir, `g${generation}.sock`));
|
|
212
|
+
const endpoint = await startMcpEndpoint({
|
|
213
|
+
socket,
|
|
214
|
+
capability,
|
|
215
|
+
generation,
|
|
216
|
+
runtime,
|
|
217
|
+
client,
|
|
218
|
+
identities,
|
|
219
|
+
remoteDaemonFiles: true,
|
|
220
|
+
});
|
|
221
|
+
partialEndpoint = endpoint;
|
|
222
|
+
const descriptor = join(bridgeDir, 'descriptor.json');
|
|
223
|
+
atomicPrivateWrite(descriptor, { socket, capability, generation, role: role.name, identity: role.identity, cid: runtime.snapshot.cid });
|
|
224
|
+
return {
|
|
225
|
+
runtime,
|
|
226
|
+
descriptor,
|
|
227
|
+
privatePaths: [root, profile.configPath, profile.credentialPath],
|
|
228
|
+
close: async (terminal) => {
|
|
229
|
+
try {
|
|
230
|
+
await endpoint.close();
|
|
231
|
+
if (terminal)
|
|
232
|
+
await runtime.terminal();
|
|
233
|
+
else
|
|
234
|
+
await runtime.suspend();
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
try {
|
|
238
|
+
await client.close();
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
controller.unlock();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
try {
|
|
249
|
+
await partialEndpoint?.close();
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
try {
|
|
253
|
+
await closeClient?.();
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
controller.unlock();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** Explicit trusted inventory migration: read and pin existing permanent CID, never bind. */
|
|
263
|
+
export async function preparePermanentAssignment(role) {
|
|
264
|
+
const profile = readClientProfile({ ...process.env, ...role.env });
|
|
265
|
+
if (!profile)
|
|
266
|
+
throw Error('MANAGED_OURS_REQUIRES_EXPLICIT_DAEMON_PROFILE');
|
|
267
|
+
const client = await attachOursClient({
|
|
268
|
+
endpoint: profile.endpoint,
|
|
269
|
+
expectedInstanceId: profile.expectedInstanceId,
|
|
270
|
+
credentialPath: profile.credentialPath,
|
|
271
|
+
sessionMode: 'external',
|
|
272
|
+
leaseToken: randomUUID(),
|
|
273
|
+
requiredCapabilities: ['external-sessions-v1', 'root-first-identities-v1'],
|
|
274
|
+
env: {},
|
|
275
|
+
});
|
|
276
|
+
try {
|
|
277
|
+
const rows = await client.listIdentities(), row = rows.find((r) => r.name === role.identity);
|
|
278
|
+
if (!row)
|
|
279
|
+
return 'unverified';
|
|
280
|
+
if (!('cid' in row) || row.kind !== 'role' || row.temp)
|
|
281
|
+
throw Error('PERMANENT_ASSIGNMENT_NOT_A_ROLE');
|
|
282
|
+
const dir = join(privateRuntimeRoot(), binderKey(profile.expectedInstanceId, role.identity));
|
|
283
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
284
|
+
const path = join(dir, 'identity-pin.json');
|
|
285
|
+
if (existsSync(path)) {
|
|
286
|
+
const old = JSON.parse(readFileSync(path, 'utf8'));
|
|
287
|
+
if (old.cid !== row.cid ||
|
|
288
|
+
old.daemon !== profile.expectedInstanceId ||
|
|
289
|
+
old.name !== role.identity)
|
|
290
|
+
throw Error('PERMANENT_ASSIGNMENT_PIN_MISMATCH');
|
|
291
|
+
}
|
|
292
|
+
else
|
|
293
|
+
atomicPrivateWrite(path, {
|
|
294
|
+
daemon: profile.expectedInstanceId,
|
|
295
|
+
name: role.identity,
|
|
296
|
+
cid: row.cid,
|
|
297
|
+
});
|
|
298
|
+
return 'verified';
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
try {
|
|
302
|
+
await client.releaseLease();
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
await client.close();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
export function readRoomReadiness(roomCid, name) {
|
|
310
|
+
try {
|
|
311
|
+
return JSON.parse(readFileSync(join(privateRuntimeRoot(), 'room-inputs', binderKey(roomCid, name) + '.ready.json'), 'utf8'));
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (error.code === 'ENOENT')
|
|
315
|
+
return;
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/** Explicit logical-instance termination, independent of harness/bridge transport. */
|
|
320
|
+
export async function releaseManagedAgent(role) {
|
|
321
|
+
const profile = readClientProfile({ ...process.env, ...role.env });
|
|
322
|
+
if (!profile)
|
|
323
|
+
throw Error('MANAGED_OURS_REQUIRES_EXPLICIT_DAEMON_PROFILE');
|
|
324
|
+
const root = privateRuntimeRoot(), dir = join(root, binderKey(profile.expectedInstanceId, role.identity));
|
|
325
|
+
if (!existsSync(join(dir, 'instance.json')))
|
|
326
|
+
return;
|
|
327
|
+
const instance = JSON.parse(readFileSync(join(dir, 'instance.json'), 'utf8'));
|
|
328
|
+
if (instance.role !== role.name)
|
|
329
|
+
throw Error('IDENTITY_ASSIGNED_TO_OTHER_ROLE');
|
|
330
|
+
const controller = await RuntimeController.acquire(root, profile.expectedInstanceId, role.identity, instance.instance);
|
|
331
|
+
try {
|
|
332
|
+
const state = controller.journal.read();
|
|
333
|
+
if (!state || state.phase === 'RELEASED')
|
|
334
|
+
return;
|
|
335
|
+
const owner = JSON.parse(readFileSync(join(dir, 'owner.json'), 'utf8'));
|
|
336
|
+
if (owner.instance !== state.instance)
|
|
337
|
+
throw Error('OWNER_RECORD_MISMATCH');
|
|
338
|
+
const client = await attachOursClient({
|
|
339
|
+
endpoint: profile.endpoint,
|
|
340
|
+
expectedInstanceId: profile.expectedInstanceId,
|
|
341
|
+
credentialPath: profile.credentialPath,
|
|
342
|
+
sessionMode: 'external',
|
|
343
|
+
leaseToken: owner.token,
|
|
344
|
+
requiredCapabilities: ['external-sessions-v1', 'root-first-identities-v1'],
|
|
345
|
+
});
|
|
346
|
+
try {
|
|
347
|
+
const runtime = new AgentOursRuntime({
|
|
348
|
+
instance: state.instance,
|
|
349
|
+
generation: state.generation,
|
|
350
|
+
daemon: state.daemon,
|
|
351
|
+
name: state.name,
|
|
352
|
+
lifetime: state.lifetime,
|
|
353
|
+
action: state.action,
|
|
354
|
+
allowCreate: false,
|
|
355
|
+
bio: '',
|
|
356
|
+
}, {
|
|
357
|
+
client,
|
|
358
|
+
journal: controller.journal,
|
|
359
|
+
assertFence: controller.assertFence,
|
|
360
|
+
now: Date.now,
|
|
361
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
362
|
+
});
|
|
363
|
+
await runtime.terminal();
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
await client.close();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
controller.unlock();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type Phase = 'PREPARING' | 'OWNED' | 'ROOM_PENDING' | 'READY' | 'SERVING' | 'RECOVERING' | 'QUIESCING' | 'TERMINAL_INTENT' | 'RELEASED' | 'FAILED' | 'CLEANUP_PENDING';
|
|
2
|
+
export interface RuntimeState {
|
|
3
|
+
version: 1;
|
|
4
|
+
instance: string;
|
|
5
|
+
generation: number;
|
|
6
|
+
daemon: string;
|
|
7
|
+
name: string;
|
|
8
|
+
cid?: string;
|
|
9
|
+
lifetime: 'permanent' | 'temporary';
|
|
10
|
+
action: string;
|
|
11
|
+
phase: Phase;
|
|
12
|
+
revision: number;
|
|
13
|
+
updatedAt: string;
|
|
14
|
+
admissionIntent?: {
|
|
15
|
+
id: string;
|
|
16
|
+
cid: string;
|
|
17
|
+
seat: string;
|
|
18
|
+
action: string;
|
|
19
|
+
agentCid: string;
|
|
20
|
+
};
|
|
21
|
+
releaseAck?: {
|
|
22
|
+
released: string[];
|
|
23
|
+
closed: string[];
|
|
24
|
+
attempted: number;
|
|
25
|
+
notified: number;
|
|
26
|
+
failed: number;
|
|
27
|
+
};
|
|
28
|
+
room?: {
|
|
29
|
+
id: string;
|
|
30
|
+
cid: string;
|
|
31
|
+
seat: string;
|
|
32
|
+
agentCid: string;
|
|
33
|
+
action: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export declare function atomicPrivateWrite(path: string, value: unknown): void;
|
|
37
|
+
export declare function binderKey(daemon: string, identity: string): string;
|
|
38
|
+
/** Supervisor-owned durable state; retained across harness reconnects. */
|
|
39
|
+
export declare class RuntimeJournal {
|
|
40
|
+
readonly privateDir: string;
|
|
41
|
+
readonly path: string;
|
|
42
|
+
constructor(privateDir: string);
|
|
43
|
+
read(): RuntimeState | undefined;
|
|
44
|
+
commit(next: RuntimeState, expectedRevision?: number): void;
|
|
45
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const phases = [
|
|
5
|
+
'PREPARING',
|
|
6
|
+
'OWNED',
|
|
7
|
+
'ROOM_PENDING',
|
|
8
|
+
'READY',
|
|
9
|
+
'SERVING',
|
|
10
|
+
'RECOVERING',
|
|
11
|
+
'QUIESCING',
|
|
12
|
+
'TERMINAL_INTENT',
|
|
13
|
+
'RELEASED',
|
|
14
|
+
'FAILED',
|
|
15
|
+
'CLEANUP_PENDING',
|
|
16
|
+
];
|
|
17
|
+
export function atomicPrivateWrite(path, value) {
|
|
18
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
19
|
+
const fd = openSync(tmp, 'wx', 0o600);
|
|
20
|
+
try {
|
|
21
|
+
writeFileSync(fd, JSON.stringify(value) + '\n');
|
|
22
|
+
fsyncSync(fd);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
closeSync(fd);
|
|
26
|
+
}
|
|
27
|
+
renameSync(tmp, path);
|
|
28
|
+
const parent = openSync(join(path, '..'), 'r');
|
|
29
|
+
try {
|
|
30
|
+
fsyncSync(parent);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
closeSync(parent);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function binderKey(daemon, identity) {
|
|
37
|
+
return createHash('sha256')
|
|
38
|
+
.update(JSON.stringify([daemon, identity]))
|
|
39
|
+
.digest('hex');
|
|
40
|
+
}
|
|
41
|
+
/** Supervisor-owned durable state; retained across harness reconnects. */
|
|
42
|
+
export class RuntimeJournal {
|
|
43
|
+
privateDir;
|
|
44
|
+
path;
|
|
45
|
+
constructor(privateDir) {
|
|
46
|
+
this.privateDir = privateDir;
|
|
47
|
+
mkdirSync(privateDir, { recursive: true, mode: 0o700 });
|
|
48
|
+
this.path = join(privateDir, 'state.json');
|
|
49
|
+
}
|
|
50
|
+
read() {
|
|
51
|
+
let raw;
|
|
52
|
+
try {
|
|
53
|
+
raw = readFileSync(this.path, 'utf8');
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error.code === 'ENOENT')
|
|
57
|
+
return;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
const s = JSON.parse(raw);
|
|
61
|
+
if (s.version !== 1 ||
|
|
62
|
+
!s.instance ||
|
|
63
|
+
!s.daemon ||
|
|
64
|
+
!s.name ||
|
|
65
|
+
!s.action ||
|
|
66
|
+
!phases.includes(s.phase) ||
|
|
67
|
+
!Number.isSafeInteger(s.generation) ||
|
|
68
|
+
s.generation < 1 ||
|
|
69
|
+
!Number.isSafeInteger(s.revision) ||
|
|
70
|
+
!['permanent', 'temporary'].includes(s.lifetime))
|
|
71
|
+
throw Error('CORRUPT_RUNTIME_JOURNAL');
|
|
72
|
+
return s;
|
|
73
|
+
}
|
|
74
|
+
commit(next, expectedRevision) {
|
|
75
|
+
const current = this.read();
|
|
76
|
+
if (current?.revision !== expectedRevision)
|
|
77
|
+
throw Error('STALE_RUNTIME_REVISION');
|
|
78
|
+
if (current &&
|
|
79
|
+
(current.instance !== next.instance ||
|
|
80
|
+
current.daemon !== next.daemon ||
|
|
81
|
+
current.name !== next.name ||
|
|
82
|
+
current.lifetime !== next.lifetime ||
|
|
83
|
+
next.generation < current.generation))
|
|
84
|
+
throw Error('RUNTIME_IDENTITY_MISMATCH');
|
|
85
|
+
if (current &&
|
|
86
|
+
['TERMINAL_INTENT', 'CLEANUP_PENDING', 'RELEASED'].includes(current.phase) &&
|
|
87
|
+
!['TERMINAL_INTENT', 'CLEANUP_PENDING', 'RELEASED'].includes(next.phase))
|
|
88
|
+
throw Error('RETIRED_RUNTIME');
|
|
89
|
+
atomicPrivateWrite(this.path, {
|
|
90
|
+
...next,
|
|
91
|
+
revision: (expectedRevision ?? 0) + 1,
|
|
92
|
+
updatedAt: new Date().toISOString(),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Socket } from 'node:net';
|
|
2
|
+
export declare const MAX_FRAME_BYTES: number;
|
|
3
|
+
export declare const MAX_CHUNK_BYTES: number;
|
|
4
|
+
/** One bounded framed channel. No request replay after disconnect. */
|
|
5
|
+
export declare class Wire {
|
|
6
|
+
readonly socket: Socket;
|
|
7
|
+
private buffer;
|
|
8
|
+
private ended;
|
|
9
|
+
onFrame: (frame: unknown) => void;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
constructor(socket: Socket);
|
|
12
|
+
send(frame: unknown): Promise<void>;
|
|
13
|
+
close(): void;
|
|
14
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const MAX_FRAME_BYTES = 1024 * 1024;
|
|
2
|
+
export const MAX_CHUNK_BYTES = 64 * 1024;
|
|
3
|
+
/** One bounded framed channel. No request replay after disconnect. */
|
|
4
|
+
export class Wire {
|
|
5
|
+
socket;
|
|
6
|
+
buffer = Buffer.alloc(0);
|
|
7
|
+
ended = false;
|
|
8
|
+
onFrame = () => { };
|
|
9
|
+
onClose = () => { };
|
|
10
|
+
constructor(socket) {
|
|
11
|
+
this.socket = socket;
|
|
12
|
+
socket.on('data', (chunk) => {
|
|
13
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
14
|
+
for (;;) {
|
|
15
|
+
const end = this.buffer.indexOf(10);
|
|
16
|
+
if (end < 0) {
|
|
17
|
+
if (this.buffer.length > MAX_FRAME_BYTES)
|
|
18
|
+
socket.destroy();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (end > MAX_FRAME_BYTES) {
|
|
22
|
+
socket.destroy();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const line = this.buffer.subarray(0, end);
|
|
26
|
+
this.buffer = this.buffer.subarray(end + 1);
|
|
27
|
+
try {
|
|
28
|
+
this.onFrame(JSON.parse(line.toString('utf8')));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
socket.destroy();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
socket.on('error', () => socket.destroy());
|
|
37
|
+
socket.on('close', () => {
|
|
38
|
+
if (!this.ended) {
|
|
39
|
+
this.ended = true;
|
|
40
|
+
this.onClose();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async send(frame) {
|
|
45
|
+
if (this.ended || this.socket.destroyed)
|
|
46
|
+
throw Error('BRIDGE_DISCONNECTED');
|
|
47
|
+
const bytes = Buffer.from(JSON.stringify(frame) + '\n');
|
|
48
|
+
if (bytes.length > MAX_FRAME_BYTES)
|
|
49
|
+
throw Error('FRAME_TOO_LARGE');
|
|
50
|
+
await new Promise((resolve, reject) => this.socket.write(bytes, (error) => (error ? reject(error) : resolve())));
|
|
51
|
+
}
|
|
52
|
+
close() {
|
|
53
|
+
this.socket.destroy();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ConversationEventV1 } from '../session/conversation-types.js';
|
|
2
|
+
import type { ConversationPageView } from './session-control.js';
|
|
3
|
+
/** Whether a ledger event belongs in the conversation: the live generation, or history of the resumed ACP session. */
|
|
4
|
+
export declare function conversationEventVisible(event: ConversationEventV1, sessionGeneration: string, resumedSessionId?: string): boolean;
|
|
5
|
+
export interface ConversationTailPage extends ConversationPageView {
|
|
6
|
+
/** Pass as `before` to read the next older page. */
|
|
7
|
+
olderCursor?: string;
|
|
8
|
+
hasOlder: boolean;
|
|
9
|
+
/** Latest page only: older events the view still needs, the current session's latest capabilities and still-pending permission requests. */
|
|
10
|
+
context: ConversationEventV1[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The newest `limit` visible events before `before` (or the newest overall),
|
|
14
|
+
* read backwards from the ledger so opening a long session costs one page.
|
|
15
|
+
* `nextCursor` is the newest ledger position seen, for forward `after` paging.
|
|
16
|
+
*/
|
|
17
|
+
export declare function conversationTailPage(stateDir: string, request: {
|
|
18
|
+
before?: string;
|
|
19
|
+
limit?: number;
|
|
20
|
+
}, snapshot: ConversationPageView['snapshot'], resumedSessionId?: string): Promise<ConversationTailPage>;
|
|
21
|
+
/** Read the existing ACP session ledger without opening its writer/recovery store. */
|
|
22
|
+
export declare function resumedConversationPage(stateDir: string, request: {
|
|
23
|
+
after?: string;
|
|
24
|
+
limit?: number;
|
|
25
|
+
}, live: ConversationPageView, expectedSessionId: string): Promise<ConversationPageView>;
|