@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
package/dist/web/service.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Exec } from '../exec.js';
|
|
2
2
|
export declare const WEB_SYSTEMD_UNIT = "ours-fleet-web.service";
|
|
3
3
|
export declare const WEB_LAUNCHD_LABEL = "network.ours.fleet.web";
|
|
4
|
+
type ServiceEnvironment = Partial<Record<'OURS_CONFIG' | 'OURS_FLEET_HOME' | 'PATH', string>>;
|
|
4
5
|
interface ServiceMetadata {
|
|
5
6
|
version: 3;
|
|
6
7
|
platform: 'linux' | 'darwin';
|
|
@@ -10,6 +11,7 @@ interface ServiceMetadata {
|
|
|
10
11
|
configuration?: string;
|
|
11
12
|
bind?: string;
|
|
12
13
|
publicOrigin?: string;
|
|
14
|
+
environment?: ServiceEnvironment;
|
|
13
15
|
}
|
|
14
16
|
export interface WebServiceOptions {
|
|
15
17
|
platform?: NodeJS.Platform;
|
|
@@ -18,6 +20,7 @@ export interface WebServiceOptions {
|
|
|
18
20
|
stateDir?: string;
|
|
19
21
|
uid?: number;
|
|
20
22
|
runtimeExecutable?: string;
|
|
23
|
+
environment?: NodeJS.ProcessEnv;
|
|
21
24
|
}
|
|
22
25
|
export declare class WebServiceManager {
|
|
23
26
|
private readonly platform;
|
|
@@ -26,6 +29,7 @@ export declare class WebServiceManager {
|
|
|
26
29
|
private readonly stateDir;
|
|
27
30
|
private readonly uid;
|
|
28
31
|
private readonly runtimeExecutable;
|
|
32
|
+
private readonly environment;
|
|
29
33
|
constructor(options?: WebServiceOptions);
|
|
30
34
|
get metadataPath(): string;
|
|
31
35
|
get definitionPath(): string;
|
|
@@ -45,9 +49,9 @@ export declare class WebServiceManager {
|
|
|
45
49
|
export declare function systemdUnit(runtime: string, script: string, port: number, configuration?: string, web?: {
|
|
46
50
|
bind?: string;
|
|
47
51
|
publicOrigin?: string;
|
|
48
|
-
}): string;
|
|
52
|
+
}, environment?: ServiceEnvironment): string;
|
|
49
53
|
export declare function launchdPlist(runtime: string, script: string, port: number, configuration?: string, web?: {
|
|
50
54
|
bind?: string;
|
|
51
55
|
publicOrigin?: string;
|
|
52
|
-
}): string;
|
|
56
|
+
}, environment?: ServiceEnvironment): string;
|
|
53
57
|
export {};
|
package/dist/web/service.js
CHANGED
|
@@ -15,6 +15,7 @@ export class WebServiceManager {
|
|
|
15
15
|
stateDir;
|
|
16
16
|
uid;
|
|
17
17
|
runtimeExecutable;
|
|
18
|
+
environment;
|
|
18
19
|
constructor(options = {}) {
|
|
19
20
|
const platform = options.platform ?? process.platform;
|
|
20
21
|
if (platform !== 'linux' && platform !== 'darwin')
|
|
@@ -25,6 +26,7 @@ export class WebServiceManager {
|
|
|
25
26
|
this.stateDir = options.stateDir ?? join(stateRoot(), 'web');
|
|
26
27
|
this.uid = options.uid ?? process.getuid?.() ?? 501;
|
|
27
28
|
this.runtimeExecutable = options.runtimeExecutable ?? process.execPath;
|
|
29
|
+
this.environment = serviceEnvironment(options.environment ?? process.env);
|
|
28
30
|
}
|
|
29
31
|
get metadataPath() { return join(this.stateDir, 'service.json'); }
|
|
30
32
|
get definitionPath() {
|
|
@@ -42,13 +44,14 @@ export class WebServiceManager {
|
|
|
42
44
|
const config = configuration ? resolve(configuration) : undefined;
|
|
43
45
|
const metadata = {
|
|
44
46
|
version: VERSION, platform: this.platform, runtime, script: resolvedScript, port,
|
|
47
|
+
environment: this.environment,
|
|
45
48
|
...(config ? { configuration: config } : {}),
|
|
46
49
|
...(web.bind ? { bind: web.bind } : {}),
|
|
47
50
|
...(web.publicOrigin ? { publicOrigin: web.publicOrigin } : {}),
|
|
48
51
|
};
|
|
49
52
|
replaceFileAtomically(this.definitionPath, this.platform === 'linux'
|
|
50
|
-
? systemdUnit(runtime, resolvedScript, port, config, web)
|
|
51
|
-
: launchdPlist(runtime, resolvedScript, port, config, web), 0o600);
|
|
53
|
+
? systemdUnit(runtime, resolvedScript, port, config, web, this.environment)
|
|
54
|
+
: launchdPlist(runtime, resolvedScript, port, config, web, this.environment), 0o600);
|
|
52
55
|
replaceFileAtomically(this.metadataPath, JSON.stringify(metadata, null, 2) + '\n', 0o600);
|
|
53
56
|
if (this.platform === 'linux') {
|
|
54
57
|
await this.must('systemctl', ['--user', 'daemon-reload']);
|
|
@@ -159,19 +162,20 @@ function validatePort(port) {
|
|
|
159
162
|
function systemdQuote(value) {
|
|
160
163
|
return `"${value.replace(/[%\\"]/g, char => char === '%' ? '%%' : `\\${char}`)}"`;
|
|
161
164
|
}
|
|
162
|
-
export function systemdUnit(runtime, script, port, configuration, web = {}) {
|
|
165
|
+
export function systemdUnit(runtime, script, port, configuration, web = {}, environment = {}) {
|
|
163
166
|
const config = configuration ? ` --configuration ${systemdQuote(configuration)}` : '';
|
|
164
167
|
const access = `${web.bind ? ` --bind ${systemdQuote(web.bind)}` : ''}`
|
|
165
168
|
+ `${web.publicOrigin ? ` --public-origin ${systemdQuote(web.publicOrigin)}` : ''}`;
|
|
166
169
|
return `[Unit]\nDescription=ours-fleet localhost web console\nAfter=default.target\n\n`
|
|
167
170
|
+ `[Service]\nType=simple\nExecStart=${systemdQuote(runtime)} ${systemdQuote(script)} web serve --port ${port} --no-open${config}${access}\n`
|
|
171
|
+
+ Object.entries(serviceEnvironment(environment)).map(([key, value]) => `Environment=${systemdQuote(`${key}=${value}`)}\n`).join('')
|
|
168
172
|
+ `Restart=on-failure\nRestartSec=5\nTimeoutStopSec=15\n\n`
|
|
169
173
|
+ `[Install]\nWantedBy=default.target\n`;
|
|
170
174
|
}
|
|
171
175
|
const xml = (value) => value.replace(/[&<>"']/g, char => ({
|
|
172
176
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
173
177
|
}[char]));
|
|
174
|
-
export function launchdPlist(runtime, script, port, configuration, web = {}) {
|
|
178
|
+
export function launchdPlist(runtime, script, port, configuration, web = {}, environment = {}) {
|
|
175
179
|
const config = configuration
|
|
176
180
|
? `<string>--configuration</string><string>${xml(configuration)}</string>` : '';
|
|
177
181
|
const access = `${web.bind ? `<string>--bind</string><string>${xml(web.bind)}</string>` : ''}`
|
|
@@ -181,6 +185,20 @@ export function launchdPlist(runtime, script, port, configuration, web = {}) {
|
|
|
181
185
|
+ `<plist version="1.0"><dict>\n<key>Label</key><string>${WEB_LAUNCHD_LABEL}</string>\n`
|
|
182
186
|
+ `<key>ProgramArguments</key><array><string>${xml(runtime)}</string><string>${xml(script)}</string><string>web</string>`
|
|
183
187
|
+ `<string>serve</string><string>--port</string><string>${port}</string><string>--no-open</string>${config}${access}</array>\n`
|
|
188
|
+
+ `<key>EnvironmentVariables</key><dict>${Object.entries(serviceEnvironment(environment)).map(([key, value]) => `<key>${key}</key><string>${xml(value)}</string>`).join('')}</dict>\n`
|
|
184
189
|
+ `<key>RunAtLoad</key><true/><key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>\n`
|
|
185
190
|
+ `<key>ProcessType</key><string>Background</string>\n</dict></plist>\n`;
|
|
186
191
|
}
|
|
192
|
+
// Retain only profile/tool paths, never inherited credentials or arbitrary environment.
|
|
193
|
+
function serviceEnvironment(source) {
|
|
194
|
+
const result = {};
|
|
195
|
+
for (const key of ['OURS_CONFIG', 'OURS_FLEET_HOME', 'PATH']) {
|
|
196
|
+
const value = source[key];
|
|
197
|
+
if (!value)
|
|
198
|
+
continue;
|
|
199
|
+
if (/[\r\n\0]/.test(value))
|
|
200
|
+
throw new FleetError('invalid_request', `invalid service path setting: ${key}`);
|
|
201
|
+
result[key] = key === 'PATH' ? value : resolve(value);
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface WorkspaceDevice {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
createdAt: number;
|
|
5
|
+
lastUsedAt: number;
|
|
6
|
+
expiresAt: number;
|
|
7
|
+
revokedAt: number | null;
|
|
8
|
+
}
|
|
9
|
+
export declare class WorkspaceDeviceStore {
|
|
10
|
+
private readonly now;
|
|
11
|
+
private readonly db;
|
|
12
|
+
readonly workspaceId: string;
|
|
13
|
+
constructor(dir?: string, now?: () => number);
|
|
14
|
+
mint(issuer?: string | null): {
|
|
15
|
+
enrollment: string;
|
|
16
|
+
workspaceId: string;
|
|
17
|
+
expiresAt: number;
|
|
18
|
+
};
|
|
19
|
+
enroll(enrollment: string, workspaceId: string, label: string): {
|
|
20
|
+
token: string;
|
|
21
|
+
device: WorkspaceDevice;
|
|
22
|
+
workspaceId: string;
|
|
23
|
+
};
|
|
24
|
+
authenticate(token: string): WorkspaceDevice;
|
|
25
|
+
valid(id: string): boolean;
|
|
26
|
+
list(): WorkspaceDevice[];
|
|
27
|
+
revoke(id: string): void;
|
|
28
|
+
close(): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { mkdirSync, chmodSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { stateRoot } from '../paths.js';
|
|
6
|
+
import { FleetError } from '../application/errors.js';
|
|
7
|
+
const opaque = () => randomBytes(32).toString('base64url');
|
|
8
|
+
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
9
|
+
export class WorkspaceDeviceStore {
|
|
10
|
+
now;
|
|
11
|
+
db;
|
|
12
|
+
workspaceId;
|
|
13
|
+
constructor(dir = join(stateRoot(), 'web'), now = Date.now) {
|
|
14
|
+
this.now = now;
|
|
15
|
+
const { DatabaseSync } = createRequire(import.meta.url)('node:sqlite');
|
|
16
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
17
|
+
chmodSync(dir, 0o700);
|
|
18
|
+
const path = join(dir, 'workspace-devices.db');
|
|
19
|
+
this.db = new DatabaseSync(path);
|
|
20
|
+
chmodSync(path, 0o600);
|
|
21
|
+
this.db.exec(`PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;
|
|
22
|
+
CREATE TABLE IF NOT EXISTS metadata(key TEXT PRIMARY KEY,value TEXT NOT NULL);
|
|
23
|
+
CREATE TABLE IF NOT EXISTS devices(id TEXT PRIMARY KEY,hash TEXT NOT NULL,label TEXT NOT NULL,createdAt INTEGER NOT NULL,lastUsedAt INTEGER NOT NULL,expiresAt INTEGER NOT NULL,revokedAt INTEGER);
|
|
24
|
+
CREATE TABLE IF NOT EXISTS enrollments(hash TEXT PRIMARY KEY,expiresAt INTEGER NOT NULL,issuer TEXT);`);
|
|
25
|
+
this.db.prepare('INSERT OR IGNORE INTO metadata VALUES(?,?)').run('workspaceId', opaque());
|
|
26
|
+
this.workspaceId = String(this.db.prepare('SELECT value FROM metadata WHERE key=?').get('workspaceId').value);
|
|
27
|
+
}
|
|
28
|
+
mint(issuer = null) {
|
|
29
|
+
const now = this.now(), enrollment = opaque(), expiresAt = now + 5 * 60000;
|
|
30
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
31
|
+
try {
|
|
32
|
+
this.db.prepare('DELETE FROM enrollments WHERE expiresAt<=?').run(now);
|
|
33
|
+
const count = Number(this.db.prepare('SELECT count(*) AS n FROM enrollments').get().n);
|
|
34
|
+
if (count >= 32)
|
|
35
|
+
throw new FleetError('rate_limited', 'too many pending device links');
|
|
36
|
+
this.db.prepare('INSERT INTO enrollments VALUES(?,?,?)').run(hash(enrollment), expiresAt, issuer);
|
|
37
|
+
this.db.exec('COMMIT');
|
|
38
|
+
return { enrollment, workspaceId: this.workspaceId, expiresAt };
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
this.db.exec('ROLLBACK');
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
enroll(enrollment, workspaceId, label) {
|
|
46
|
+
if (workspaceId !== this.workspaceId || typeof enrollment !== 'string' || !/^[\w-]{43}$/.test(enrollment))
|
|
47
|
+
throw new FleetError('unauthorized', 'invalid device enrollment');
|
|
48
|
+
if (typeof label !== 'string' || !label.trim() || label.length > 100 || /[\x00-\x1f\x7f]/.test(label))
|
|
49
|
+
throw new FleetError('invalid_request', 'device name must contain 1–100 printable characters');
|
|
50
|
+
const now = this.now(), id = opaque(), token = `${id}.${opaque()}`;
|
|
51
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
52
|
+
try {
|
|
53
|
+
const code = this.db.prepare('DELETE FROM enrollments WHERE hash=? AND expiresAt>? RETURNING issuer').get(hash(enrollment), now);
|
|
54
|
+
if (!code)
|
|
55
|
+
throw new FleetError('unauthorized', 'device enrollment is expired or already used');
|
|
56
|
+
if (code.issuer && !this.db.prepare('SELECT id FROM devices WHERE id=? AND revokedAt IS NULL AND expiresAt>?').get(code.issuer, now))
|
|
57
|
+
throw new FleetError('unauthorized', 'issuing device has been revoked');
|
|
58
|
+
const count = Number(this.db.prepare('SELECT count(*) AS n FROM devices WHERE revokedAt IS NULL AND expiresAt>?').get(now).n);
|
|
59
|
+
if (count >= 128)
|
|
60
|
+
throw new FleetError('rate_limited', 'authorized device limit reached');
|
|
61
|
+
const device = { id, label: label.trim(), createdAt: now, lastUsedAt: now, expiresAt: now + 90 * 86400000, revokedAt: null };
|
|
62
|
+
this.db.prepare('INSERT INTO devices VALUES(?,?,?,?,?,?,NULL)').run(id, hash(token), device.label, now, now, device.expiresAt);
|
|
63
|
+
this.db.exec('COMMIT');
|
|
64
|
+
return { token, device, workspaceId };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
this.db.exec('ROLLBACK');
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
authenticate(token) {
|
|
72
|
+
const id = typeof token === 'string' ? token.split('.')[0] : '', now = this.now();
|
|
73
|
+
const row = this.db.prepare('SELECT * FROM devices WHERE id=? AND revokedAt IS NULL AND expiresAt>?').get(id, now);
|
|
74
|
+
const supplied = Buffer.from(hash(token || '')), expected = Buffer.from(String(row?.hash || '0'.repeat(64)));
|
|
75
|
+
if (!row || !timingSafeEqual(supplied, expected))
|
|
76
|
+
throw new FleetError('unauthorized', 'workspace device is missing, expired, or revoked');
|
|
77
|
+
this.db.prepare('UPDATE devices SET lastUsedAt=? WHERE id=?').run(now, id);
|
|
78
|
+
return { id, label: String(row.label), createdAt: Number(row.createdAt), lastUsedAt: now, expiresAt: Number(row.expiresAt), revokedAt: null };
|
|
79
|
+
}
|
|
80
|
+
valid(id) { return Boolean(this.db.prepare('SELECT id FROM devices WHERE id=? AND revokedAt IS NULL AND expiresAt>?').get(id, this.now())); }
|
|
81
|
+
list() { return this.db.prepare('SELECT id,label,createdAt,lastUsedAt,expiresAt,revokedAt FROM devices ORDER BY createdAt DESC LIMIT 256').all().map(row => ({ id: String(row.id), label: String(row.label), createdAt: Number(row.createdAt), lastUsedAt: Number(row.lastUsedAt), expiresAt: Number(row.expiresAt), revokedAt: row.revokedAt === null ? null : Number(row.revokedAt) })); }
|
|
82
|
+
revoke(id) { this.db.prepare('UPDATE devices SET revokedAt=? WHERE id=? AND revokedAt IS NULL').run(this.now(), id); this.db.prepare('DELETE FROM enrollments WHERE issuer=?').run(id); }
|
|
83
|
+
close() { this.db.close(); }
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startWorkspaceTunnel(origin: string): () => Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { stateRoot } from '../paths.js';
|
|
5
|
+
export function startWorkspaceTunnel(origin) {
|
|
6
|
+
const dir = join(stateRoot(), 'workspace'), file = join(dir, 'tunnel.json');
|
|
7
|
+
if (!existsSync(file))
|
|
8
|
+
return async () => { };
|
|
9
|
+
const config = JSON.parse(readFileSync(file, 'utf8'));
|
|
10
|
+
if (config.origin !== origin)
|
|
11
|
+
throw Error('Workspace tunnel requires its configured public origin');
|
|
12
|
+
const tokenFile = join(dir, 'connector');
|
|
13
|
+
if (config.tokenFile !== tokenFile)
|
|
14
|
+
throw Error('Unexpected connector credential path');
|
|
15
|
+
const stat = lstatSync(tokenFile);
|
|
16
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())
|
|
17
|
+
throw Error('Connector credential must be an owned private file');
|
|
18
|
+
let stopped = false, child, timer, backoff = 1000;
|
|
19
|
+
const launch = () => {
|
|
20
|
+
if (stopped)
|
|
21
|
+
return;
|
|
22
|
+
child = spawn('cloudflared', ['tunnel', '--no-autoupdate', 'run', '--token-file', tokenFile], { stdio: 'ignore' });
|
|
23
|
+
let scheduled = false;
|
|
24
|
+
const retry = () => { if (scheduled || stopped)
|
|
25
|
+
return; scheduled = true; timer = setTimeout(launch, backoff); backoff = Math.min(30000, backoff * 2); };
|
|
26
|
+
child.once('error', retry);
|
|
27
|
+
child.once('exit', retry);
|
|
28
|
+
};
|
|
29
|
+
launch();
|
|
30
|
+
return async () => { stopped = true; clearTimeout(timer); const current = child; if (!current || current.exitCode !== null)
|
|
31
|
+
return; await new Promise(resolve => { const timeout = setTimeout(() => { current.kill('SIGKILL'); resolve(); }, 5000); current.once('exit', () => { clearTimeout(timeout); resolve(); }); current.kill('SIGTERM'); }); };
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface WorkspacePayload {
|
|
2
|
+
version: 1;
|
|
3
|
+
appOrigin: string;
|
|
4
|
+
hostname: string;
|
|
5
|
+
rootName: string;
|
|
6
|
+
name: string;
|
|
7
|
+
surname: string;
|
|
8
|
+
connectorToken: string;
|
|
9
|
+
invitation: string;
|
|
10
|
+
serverCid: string;
|
|
11
|
+
challenge: {
|
|
12
|
+
nonce: string;
|
|
13
|
+
workspaceId: string;
|
|
14
|
+
accountId: string;
|
|
15
|
+
expiresAt: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare function readWorkspacePayload(file: string): WorkspacePayload;
|
|
19
|
+
export declare function enrollWorkspace(payload: WorkspacePayload, configuration?: string, options?: {
|
|
20
|
+
migrateAppOrigin?: boolean;
|
|
21
|
+
preserveProfile?: boolean;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
origin: string;
|
|
24
|
+
hostWorkspaceId: string;
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { validateAccountOrigin } from './account-origin.js';
|
|
2
|
+
import { lstatSync, readFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { readClientProfile } from './client-profile.js';
|
|
5
|
+
import { stateRoot, defaultConfigPath } from './paths.js';
|
|
6
|
+
import { replaceFileAtomically } from './atomic-file.js';
|
|
7
|
+
import { loadConfig, splitRootFor } from './config.js';
|
|
8
|
+
import { stringify } from 'yaml';
|
|
9
|
+
import { WebAccessStore } from './web/access.js';
|
|
10
|
+
import { WorkspaceDeviceStore } from './web/workspace-devices.js';
|
|
11
|
+
export function readWorkspacePayload(file) {
|
|
12
|
+
const path = resolve(file), stat = lstatSync(path);
|
|
13
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.uid !== process.getuid?.() || (stat.mode & 0o077) !== 0 || stat.size > 32768)
|
|
14
|
+
throw Error('Setup payload requires an owned private regular file (chmod 600)');
|
|
15
|
+
let p;
|
|
16
|
+
try {
|
|
17
|
+
p = JSON.parse(Buffer.from(readFileSync(path, 'utf8').trim(), 'base64url').toString());
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw Error('Invalid workspace payload');
|
|
21
|
+
}
|
|
22
|
+
if (p.version !== 1 || !['https://app.ours.network', 'https://app.ours-tunnel.com'].includes(p.appOrigin) || !/^[a-z0-9][a-z0-9-]{2,60}\.ours-tunnel\.com$/.test(p.hostname) || !/^[a-z0-9-]{2,30}@[a-z0-9-]{2,30}$/.test(p.rootName) || !/^[a-f0-9]{64}$/i.test(p.serverCid) || !p.challenge || !['nonce', 'accountId', 'workspaceId'].every(k => /^[\w-]{43}$/.test(String(p.challenge[k]))) || p.challenge.expiresAt <= Date.now() || p.challenge.expiresAt > Date.now() + 16 * 60000)
|
|
23
|
+
throw Error('Workspace payload is invalid or expired');
|
|
24
|
+
for (const k of ['connectorToken', 'invitation', 'name', 'surname'])
|
|
25
|
+
if (typeof p[k] !== 'string' || !p[k] || p[k].length > 8192 || /[\x00-\x1f\x7f]/.test(p[k]))
|
|
26
|
+
throw Error('Invalid workspace payload');
|
|
27
|
+
return p;
|
|
28
|
+
}
|
|
29
|
+
export async function enrollWorkspace(payload, configuration = defaultConfigPath(), options = {}) {
|
|
30
|
+
const profile = readClientProfile();
|
|
31
|
+
const base = new URL(profile.endpoint);
|
|
32
|
+
base.pathname = base.pathname.replace(/\/daemon\/?$/, '/messenger/');
|
|
33
|
+
if (!base.pathname.endsWith('/messenger/'))
|
|
34
|
+
throw Error('Workspace setup requires the supported gateway client profile');
|
|
35
|
+
const secret = readFileSync(profile.credentialPath, 'utf8').trim();
|
|
36
|
+
const request = async (path, value) => { const response = await fetch(new URL('api/' + path, base), { method: value === undefined ? 'GET' : 'POST', headers: { 'X-Ours-Api-Token': secret, ...(value === undefined ? {} : { 'Content-Type': 'application/json', 'Origin': base.origin, 'X-Ours-Messenger-CSRF': '1' }) }, ...(value === undefined ? {} : { body: JSON.stringify(value) }), signal: AbortSignal.timeout(15000), redirect: 'error' }); if (!response.ok) {
|
|
37
|
+
await response.body?.cancel();
|
|
38
|
+
throw Error('Workspace Messenger enrollment failed (HTTP ' + response.status + '); request a fresh setup payload before retrying');
|
|
39
|
+
} return response.json(); };
|
|
40
|
+
if (new WebAccessStore().read().mode === 'none')
|
|
41
|
+
throw Error('Workspace enrollment requires protected web access; enable pairing or password before setup');
|
|
42
|
+
const dir = join(stateRoot(), 'workspace');
|
|
43
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
44
|
+
const appOrigin = validateAccountOrigin(payload.appOrigin);
|
|
45
|
+
const previousFile = join(dir, 'binding.json');
|
|
46
|
+
let migrating = false;
|
|
47
|
+
if (existsSync(previousFile)) {
|
|
48
|
+
const previous = JSON.parse(readFileSync(previousFile, 'utf8'));
|
|
49
|
+
if (previous.workspaceId !== payload.challenge.workspaceId)
|
|
50
|
+
throw Error('This host is already associated with another workspace');
|
|
51
|
+
const priorOrigin = validateAccountOrigin(previous.appOrigin ?? 'https://app.ours.network');
|
|
52
|
+
migrating = priorOrigin !== appOrigin;
|
|
53
|
+
if (migrating && !options.migrateAppOrigin)
|
|
54
|
+
throw Error('Account origin change requires explicit --migrate-app-origin and a fresh setup payload');
|
|
55
|
+
if (migrating && previous.serverCid && previous.serverCid.toUpperCase() !== payload.serverCid.toUpperCase())
|
|
56
|
+
throw Error('Account origin migration must retain the enrollment server identity');
|
|
57
|
+
}
|
|
58
|
+
const loaded = loadConfig(configuration);
|
|
59
|
+
if (loaded.rooms?.defaults?.attach_owner === false)
|
|
60
|
+
throw Error('Existing rooms.defaults.attach_owner is disabled; enable automatic Human Owner admission before workspace setup');
|
|
61
|
+
if (loaded.rooms && !loaded.rooms.owner.public_invite_file)
|
|
62
|
+
throw Error('Existing room owner configuration must be reviewed before workspace enrollment');
|
|
63
|
+
const identity = await request('workspace/enrollment-identity');
|
|
64
|
+
if (!/^[a-f0-9]{64}$/i.test(identity.cid))
|
|
65
|
+
throw Error('Bound Messenger identity unavailable');
|
|
66
|
+
if (options.preserveProfile && identity.preserveProfile !== true)
|
|
67
|
+
throw Error('Existing-host setup requires Messenger profile-preservation support; update the host Messenger before retrying');
|
|
68
|
+
if (loaded.rooms && loaded.rooms.owner.expected_cid.toLowerCase() !== identity.cid.toLowerCase())
|
|
69
|
+
throw Error('Retained room owner CID conflicts with the Human root');
|
|
70
|
+
const inviteFile = loaded.rooms?.owner.public_invite_file || join(dir, 'owner.invite');
|
|
71
|
+
if (existsSync(inviteFile)) {
|
|
72
|
+
const stat = lstatSync(inviteFile);
|
|
73
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || stat.nlink !== 1 || (stat.mode & 0o077) !== 0)
|
|
74
|
+
throw Error('Retained Owner invitation file must be owned and private');
|
|
75
|
+
}
|
|
76
|
+
const devices = new WorkspaceDeviceStore();
|
|
77
|
+
const hostWorkspaceId = devices.workspaceId;
|
|
78
|
+
devices.close();
|
|
79
|
+
const { name, surname, ...proofPayload } = payload;
|
|
80
|
+
const result = await request('workspace/enroll', options.preserveProfile ? { ...proofPayload, hostWorkspaceId, preserveProfile: true } : { ...payload, hostWorkspaceId });
|
|
81
|
+
if (!result.submitted || !/^[a-f0-9]{64}$/i.test(result.rootCid) || typeof result.ownerInvite !== 'string')
|
|
82
|
+
throw Error('Malformed workspace enrollment response');
|
|
83
|
+
if (loaded.rooms && loaded.rooms.owner.expected_cid.toLowerCase() !== result.rootCid.toLowerCase())
|
|
84
|
+
throw Error('Retained room owner CID conflicts with the Human root');
|
|
85
|
+
if (migrating) {
|
|
86
|
+
const deadline = Date.now() + 15000;
|
|
87
|
+
while (true) {
|
|
88
|
+
const response = await fetch(appOrigin + '/account-api/workspace-proof', { method: 'POST', headers: { Origin: appOrigin, 'Content-Type': 'application/json' }, credentials: 'omit', redirect: 'error', signal: AbortSignal.timeout(5000), body: JSON.stringify({ ...payload.challenge, hostWorkspaceId, rootCid: result.rootCid }) });
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
await response.body?.cancel();
|
|
91
|
+
throw Error('Account origin migration proof was rejected; local trust unchanged');
|
|
92
|
+
}
|
|
93
|
+
const receipt = await response.json();
|
|
94
|
+
if (receipt.verified === true)
|
|
95
|
+
break;
|
|
96
|
+
if (Date.now() >= deadline)
|
|
97
|
+
throw Error('Account origin migration proof is not confirmed; local trust unchanged');
|
|
98
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
replaceFileAtomically(inviteFile, result.ownerInvite + '\n', 0o600);
|
|
102
|
+
const split = splitRootFor(configuration);
|
|
103
|
+
mkdirSync(split, { recursive: true, mode: 0o700 });
|
|
104
|
+
if (!loaded.rooms)
|
|
105
|
+
replaceFileAtomically(configuration, readFileSync(configuration, 'utf8') + '\n' + stringify({ rooms: { owner: { provider: 'messenger-server', expected_cid: result.rootCid, public_invite_file: inviteFile, role: 'Owner' }, defaults: { attach_owner: true, close_when_task_done: true } } }), 0o600);
|
|
106
|
+
replaceFileAtomically(join(dir, 'connector'), payload.connectorToken + '\n', 0o600);
|
|
107
|
+
const origin = `https://${payload.hostname}`;
|
|
108
|
+
replaceFileAtomically(join(dir, 'tunnel.json'), JSON.stringify({ origin, tokenFile: join(dir, 'connector') }) + '\n', 0o600);
|
|
109
|
+
replaceFileAtomically(previousFile, JSON.stringify({ workspaceId: payload.challenge.workspaceId, hostWorkspaceId, appOrigin, serverCid: payload.serverCid.toUpperCase() }) + '\n', 0o600);
|
|
110
|
+
loadConfig(configuration);
|
|
111
|
+
return { origin, hostWorkspaceId };
|
|
112
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# YAML Room Layouts
|
|
2
|
+
|
|
3
|
+
Room Layout describes room goals, participants and their shared or independent
|
|
4
|
+
sessions. Rooms and agents have independent lifecycles.
|
|
5
|
+
|
|
6
|
+
Place a layout at `fleet/room_layouts/<name>.yaml` beside the split configuration
|
|
7
|
+
for `fleet.yaml`. Agent Templates remain in `fleet/agent_templates/`.
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
version: 1
|
|
11
|
+
participants:
|
|
12
|
+
product: {agent_template: Product}
|
|
13
|
+
developer: {agent_template: Developer}
|
|
14
|
+
qa: {agent_template: QA}
|
|
15
|
+
rooms:
|
|
16
|
+
discovery:
|
|
17
|
+
goal: Clarify the problem and acceptance criteria
|
|
18
|
+
members: [product, developer, qa]
|
|
19
|
+
design:
|
|
20
|
+
goal: Review implementation choices
|
|
21
|
+
members: [developer, qa]
|
|
22
|
+
amigos:
|
|
23
|
+
goal: Resolve a concrete acceptance question
|
|
24
|
+
members: [product, developer, qa]
|
|
25
|
+
roles: {product: product, developer: developer, qa: qa}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Participant `instance_scope` controls instance reuse:
|
|
29
|
+
|
|
30
|
+
- `layout` (default): repeating a participant key reuses its exact running instance
|
|
31
|
+
and session within this layout run.
|
|
32
|
+
- `room`: each room using the key gets its own identity and session, lazily created
|
|
33
|
+
when that room opens. Repeated opens of the same active room reuse that instance.
|
|
34
|
+
|
|
35
|
+
For example, one shared process doctor and one independent critic per room:
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
version: 1
|
|
39
|
+
participants:
|
|
40
|
+
doctor: {agent_template: ProcessDoctor, instance_scope: layout}
|
|
41
|
+
critic: {agent_template: Critic, instance_scope: room}
|
|
42
|
+
rooms:
|
|
43
|
+
discovery:
|
|
44
|
+
goal: Agree on the problem
|
|
45
|
+
members: [doctor, critic]
|
|
46
|
+
design:
|
|
47
|
+
goal: Review the design
|
|
48
|
+
members: [doctor, critic]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Different keys using the same Agent Template still create independent instances.
|
|
52
|
+
See the [shared](../examples/room-layouts/shared.yaml) and
|
|
53
|
+
[fresh](../examples/room-layouts/fresh.yaml) examples, and the mixed
|
|
54
|
+
[scoped](../examples/room-layouts/scoped.yaml) example. All choices can coexist.
|
|
55
|
+
Fresh agents receive their room context. Supply relevant evidence through ordinary
|
|
56
|
+
room messages or existing project documents when needed.
|
|
57
|
+
|
|
58
|
+
A participant without `agent_template` requires a binding. `layout instance`
|
|
59
|
+
returns a local exact reference (supervisor, agent, launch, CID and session).
|
|
60
|
+
Pass participant-to-reference YAML to `create --bindings`. A stopped or replaced instance is rejected. Cross-Fleet bindings require
|
|
61
|
+
an explicit owner grant, as described below.
|
|
62
|
+
Room-scoped participants require `agent_template`; an explicit binding for them
|
|
63
|
+
is rejected before creating resources. Bindings for layout-scoped participants
|
|
64
|
+
keep their existing behavior.
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
ours-fleet layout validate work
|
|
68
|
+
ours-fleet layout create work --id example
|
|
69
|
+
ours-fleet layout open example design
|
|
70
|
+
ours-fleet layout open example discovery
|
|
71
|
+
ours-fleet layout open example amigos
|
|
72
|
+
ours-fleet layout close-room example amigos
|
|
73
|
+
ours-fleet layout status example
|
|
74
|
+
ours-fleet layout close example
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Any declared room may open first. `create` only saves the definition and resolved
|
|
78
|
+
Agent Templates; `open` lazily starts missing participants and admits all members
|
|
79
|
+
before sending their room context. The membership notification is informational. Existing room rules govern when
|
|
80
|
+
an agent contributes. Each room opens through an explicit command.
|
|
81
|
+
|
|
82
|
+
`close-room` archives a room and keeps agents available to other rooms. `close`
|
|
83
|
+
archives opened rooms and stops only temporary participants created by this layout
|
|
84
|
+
instance. Explicitly bound participants survive. Cleanup processes existing resources. A 3-Amigos room is an ordinary room with
|
|
85
|
+
complementary members.
|
|
86
|
+
Room-scoped instances follow the same cleanup rules: closing a room does not
|
|
87
|
+
retire them, and a closed room cannot reopen. A new layout run creates new instances.
|
|
88
|
+
|
|
89
|
+
Private state keeps the authorable definition unchanged. Its `participants` map
|
|
90
|
+
uses the original key for shared instances and `<room>:<participant>` for
|
|
91
|
+
room-scoped instances, with `participant` and `room` metadata. Declared but unopened
|
|
92
|
+
rooms have no running instance. Status and task read models distinguish these
|
|
93
|
+
entries and resolve their original Agent Template. Existing snapshots without
|
|
94
|
+
`instance_scope` retain their original keys and require no migration. Snapshots
|
|
95
|
+
using `room` require a Fleet version that supports this field; they are not a
|
|
96
|
+
downgrade format for older Fleet versions.
|
|
97
|
+
|
|
98
|
+
Supported room fields: `goal`, `members`, optional `roles`, `contract`,
|
|
99
|
+
`quiet_membership`, `anonymous`. `contract` is briefing text.
|
|
100
|
+
Top-level fields: `version: 1`, optional `description`, `participants`, `rooms`.
|
|
101
|
+
Participant fields: optional `agent_template`, `instance_scope: layout|room`.
|
|
102
|
+
Unknown fields, ambiguous YAML and invalid member references are rejected.
|
|
103
|
+
Existing templates and tasks keep their behavior.
|
|
104
|
+
|
|
105
|
+
Local Fleet control uses the existing authenticated control socket,
|
|
106
|
+
agent runtime, `spawnTemp` and session queue. Cross-Fleet bindings delegate
|
|
107
|
+
membership operations to their owning Fleet as described below. Private state
|
|
108
|
+
snapshots template definitions; public status hides their environment values.
|
|
109
|
+
Existing owner settings apply; owner CID/invite fingerprint are pinned. Unknown
|
|
110
|
+
mutation outcomes stop new work for inspection rather than blindly replaying a
|
|
111
|
+
spawn or room creation. `close` and `close-room` can still clean known resources.
|
|
112
|
+
Cleanup continues after individual errors and does not stop replacement or borrowed
|
|
113
|
+
instances. Confirmed stopped workers need no live control socket. An unknown
|
|
114
|
+
original outcome remains recorded, and cleanup reports incomplete until inspected;
|
|
115
|
+
it never claims that unknown resources were found or removed. Retrying cleanup
|
|
116
|
+
checks known resources and skips completed cleanup. Reopening an active layout room
|
|
117
|
+
checks its native state, CID and required seats and reports drift without repairing it.
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
Shared sessions retain available conversation context, not guaranteed perfect
|
|
121
|
+
recall or isolation between rooms. Compaction and model judgment still matter.
|
|
122
|
+
Rooms keep their existing turn scheduling.
|
|
123
|
+
|
|
124
|
+
Each layout instance creates distinct native room names, so several instances can
|
|
125
|
+
open the same declared room key. Participant roles must differ from the attached
|
|
126
|
+
owner role; a collision is rejected before creating resources.
|
|
127
|
+
|
|
128
|
+
Definitions are loaded for authoring commands. Retained instances use their saved
|
|
129
|
+
layout and Agent Templates. Status and cleanup operate on saved resources even
|
|
130
|
+
when the source YAML is broken or removed. New agent launches continue to use
|
|
131
|
+
Fleet's normal configuration and launch checks.
|
|
132
|
+
|
|
133
|
+
## Borrowing an instance from another Fleet on the same host
|
|
134
|
+
|
|
135
|
+
Fleet A can open rooms using standalone temporary or persistent agents supervised
|
|
136
|
+
by Fleet B under another OS user. Both must select the same daemon instance and
|
|
137
|
+
Cowork gateway. An agent's original Fleet keeps its session, identity and lifecycle
|
|
138
|
+
control. This extends layout bindings; it does not add bindings to legacy
|
|
139
|
+
`room create` / RoomTemplate provisioning.
|
|
140
|
+
|
|
141
|
+
On the agent owner's Fleet B, use its existing web server and grant access to an
|
|
142
|
+
exact live standalone instance:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
ours-fleet web serve --port 49272 --no-open
|
|
146
|
+
# In another terminal belonging to Fleet B's owner:
|
|
147
|
+
ours-fleet layout share Architect --temporary --participant architect \
|
|
148
|
+
--server-url http://127.0.0.1:49272 --output ./architect.binding.yaml
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The command prints the grant ID and output path. It creates a binding YAML and
|
|
152
|
+
an adjacent `.token` file, both owner-only. The private owner record stores a
|
|
153
|
+
SHA-256 token digest, not the token. `share` never overwrites an existing export.
|
|
154
|
+
Transfer both files explicitly to Fleet A's user through your existing secure
|
|
155
|
+
file-transfer method, keeping the receiving credential file owned by that user
|
|
156
|
+
with mode `0600`. Do not share Fleet B's console session, client profile, daemon
|
|
157
|
+
credential, private state directory or supervisor socket. This feature creates
|
|
158
|
+
no additional server; Fleet B's existing web server must remain available.
|
|
159
|
+
|
|
160
|
+
Export each specialist under its logical layout participant key, then merge the
|
|
161
|
+
YAML mappings into one bindings document. Keep every adjacent token filename
|
|
162
|
+
unique. Relative credential paths resolve against the bindings file's directory.
|
|
163
|
+
The reusable layout contains participant factories or empty participant mappings,
|
|
164
|
+
never concrete credentials or running-instance references:
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
version: 1
|
|
168
|
+
participants:
|
|
169
|
+
architect: {}
|
|
170
|
+
developer: {}
|
|
171
|
+
doctor: {}
|
|
172
|
+
rooms:
|
|
173
|
+
product: {goal: Scope, members: [architect, doctor]}
|
|
174
|
+
design: {goal: Design, members: [architect, developer, doctor]}
|
|
175
|
+
delivery: {goal: Delivery, members: [developer, doctor]}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
On Fleet A:
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
ours-fleet layout create project --id project-run --bindings ./bindings.yaml
|
|
182
|
+
ours-fleet layout open project-run product
|
|
183
|
+
ours-fleet layout open project-run design
|
|
184
|
+
ours-fleet layout open project-run delivery
|
|
185
|
+
ours-fleet layout close-room project-run product
|
|
186
|
+
ours-fleet layout close project-run
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The exact CID, launch and harness session are reused. Reopening an active room
|
|
190
|
+
checks existing membership without delivering duplicate context. Closing rooms
|
|
191
|
+
archives Cowork history and never stops borrowed instances. After acceptance,
|
|
192
|
+
the original Fleet B owner explicitly retires its standalone temporary specialists
|
|
193
|
+
using the existing lifecycle commands. Persistent specialists keep their ordinary
|
|
194
|
+
lifecycle. Mixed local-created and borrowed participants are supported.
|
|
195
|
+
|
|
196
|
+
An exported reference adds `remote` to the existing instance reference:
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
remote:
|
|
200
|
+
url: http://127.0.0.1:49272
|
|
201
|
+
grant_id: 11111111-1111-4111-8111-111111111111
|
|
202
|
+
credential_file: architect.binding.yaml.token
|
|
203
|
+
daemon_instance_id: 22222222-2222-4222-8222-222222222222
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The grant authorizes only `verify`, `join`, and `assign` for that exact instance.
|
|
207
|
+
Room admission validates the requested room ID and CID against shared Cowork and
|
|
208
|
+
uses an invite issued by Cowork on the owner side; caller-supplied contact invites
|
|
209
|
+
are rejected before contact mutation. Context delivery also requires its active seat, expected room CID and role in
|
|
210
|
+
Cowork. It cannot inspect inventory, spawn, stop, reconfigure, export identities,
|
|
211
|
+
or authenticate browser console routes. It grants the holder room admission and
|
|
212
|
+
room-context delivery, so transfer it only to the intended room creator.
|
|
213
|
+
|
|
214
|
+
Owner-side revocation does not stop the agent or remove existing memberships:
|
|
215
|
+
|
|
216
|
+
```sh
|
|
217
|
+
ours-fleet layout revoke-binding <grant-id>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Stopped/replaced sessions, a different daemon, revoked grants or unavailable
|
|
221
|
+
owner transport produce errors; no replacement is spawned. A lost response to a
|
|
222
|
+
membership/context mutation retains the existing layout `uncertain` marker.
|
|
223
|
+
Inspect the native room and owning Fleet before retrying; no mutation is blindly
|
|
224
|
+
replayed. Only direct loopback HTTP origins (`127.0.0.1`) are supported;
|
|
225
|
+
redirects, external hosts and forwarded requests are rejected. No cross-host
|
|
226
|
+
support, discovery protocol, mission service, or room conversation limits are
|
|
227
|
+
introduced.
|