@ours.network/fleet 1.1.0-nightly.1 → 1.1.0-nightly.3
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 +76 -93
- package/dist/application/role-creation-service.d.ts +2 -21
- package/dist/application/role-creation-service.js +15 -64
- package/dist/application/role-removal-service.js +6 -3
- package/dist/application/task-room-service.js +7 -2
- package/dist/application/types.d.ts +1 -1
- package/dist/briefing.js +5 -5
- package/dist/build-info.json +4 -4
- package/dist/cli.js +48 -38
- package/dist/config.d.ts +65 -2
- package/dist/config.js +337 -25
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +2 -2
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +48 -97
- package/dist/doctor.js +4 -4
- package/dist/fleet-proxy.js +30 -8
- package/dist/generated-agent-source.d.ts +9 -0
- package/dist/generated-agent-source.js +53 -0
- package/dist/harness/agent-session.d.ts +13 -0
- package/dist/harness/claude-code-session.d.ts +4 -0
- package/dist/harness/claude-code-session.js +4 -0
- package/dist/harness/claude-code.js +10 -0
- package/dist/harness/codex-session.d.ts +4 -0
- package/dist/harness/codex-session.js +4 -0
- package/dist/harness/codex.js +14 -0
- package/dist/harness/registry.js +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/init-guidance.d.ts +1 -0
- package/dist/init-guidance.js +5 -0
- package/dist/model-env.d.ts +2 -3
- package/dist/model-env.js +3 -5
- package/dist/ops.js +4 -2
- package/dist/owner-channel/commands.js +2 -1
- package/dist/resolved-plan.d.ts +3 -2
- package/dist/resolved-plan.js +21 -5
- package/dist/rooms-tasks/cli.js +2 -1
- package/dist/rooms-tasks/config.js +24 -9
- package/dist/rooms-tasks/provision.js +71 -36
- package/dist/rooms-tasks/templates.js +6 -6
- package/dist/rooms-tasks/types.d.ts +8 -15
- package/dist/rooms-tasks/types.js +1 -4
- package/dist/sensitive-config.d.ts +2 -0
- package/dist/sensitive-config.js +4 -0
- package/dist/spawn.d.ts +8 -33
- package/dist/spawn.js +52 -211
- package/dist/watchdog/config.d.ts +10 -7
- package/dist/watchdog/config.js +29 -19
- package/dist/watchdog/run.js +4 -13
- package/dist/web/fleet-config-service.d.ts +11 -8
- package/dist/web/fleet-config-service.js +383 -183
- package/dist/web/topology-promote.js +25 -15
- package/dist/web/topology.js +2 -2
- package/dist/web/yaml-document-edit.js +2 -0
- package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
- package/dist/web-app/index.html +1 -1
- package/examples/fleet/agents/Alice.yaml +11 -0
- package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
- package/examples/fleet/brains/claude-default.yaml +5 -0
- package/examples/fleet/roles/coordinator.yaml +5 -0
- package/examples/fleet/roles/developer.yaml +3 -0
- package/examples/fleet.yaml +74 -0
- package/package.json +2 -1
- package/dist/web-app/assets/index-DhMDVRU1.js +0 -10
|
@@ -3,6 +3,8 @@ import type { AcpMcpServer, SessionPrep } from './types.js';
|
|
|
3
3
|
import type { AgentSessionAdapter, AgentSessionStartOptions, PreparedAgentSessionLaunch } from './agent-session.js';
|
|
4
4
|
import { type AcpSessionTransport } from './acp-session-transport.js';
|
|
5
5
|
export interface ClaudeCodeSessionStrategy {
|
|
6
|
+
resolveBrain: AgentSessionAdapter['resolveBrain'];
|
|
7
|
+
modelEnvironmentVariable: AgentSessionAdapter['modelEnvironmentVariable'];
|
|
6
8
|
prepareLaunch(role: ResolvedRole, prep: SessionPrep): PreparedAgentSessionLaunch;
|
|
7
9
|
permissionModeId(role: ResolvedRole): string | undefined;
|
|
8
10
|
mcpServers(role: ResolvedRole): AcpMcpServer[] | undefined;
|
|
@@ -13,6 +15,8 @@ export declare class ClaudeCodeAgentSessionAdapter implements AgentSessionAdapte
|
|
|
13
15
|
private readonly strategy;
|
|
14
16
|
private readonly transport;
|
|
15
17
|
constructor(strategy: ClaudeCodeSessionStrategy, transport?: AcpSessionTransport);
|
|
18
|
+
resolveBrain(brain: Parameters<AgentSessionAdapter['resolveBrain']>[0]): import("./agent-session.js").ResolvedBrainSelection;
|
|
19
|
+
modelEnvironmentVariable(): string | undefined;
|
|
16
20
|
prepareLaunch(role: ResolvedRole, prep: SessionPrep): PreparedAgentSessionLaunch;
|
|
17
21
|
start(options: AgentSessionStartOptions): Promise<import("../index.js").AgentSession>;
|
|
18
22
|
}
|
|
@@ -8,6 +8,10 @@ export class ClaudeCodeAgentSessionAdapter {
|
|
|
8
8
|
this.strategy = strategy;
|
|
9
9
|
this.transport = transport;
|
|
10
10
|
}
|
|
11
|
+
resolveBrain(brain) {
|
|
12
|
+
return this.strategy.resolveBrain(brain);
|
|
13
|
+
}
|
|
14
|
+
modelEnvironmentVariable() { return this.strategy.modelEnvironmentVariable(); }
|
|
11
15
|
prepareLaunch(role, prep) {
|
|
12
16
|
return this.strategy.prepareLaunch(role, prep);
|
|
13
17
|
}
|
|
@@ -228,6 +228,16 @@ export function makeClaudeCodeAdapter(exec = realExec, transport) {
|
|
|
228
228
|
return {
|
|
229
229
|
id: 'claude-code',
|
|
230
230
|
agentSession: new ClaudeCodeAgentSessionAdapter({
|
|
231
|
+
resolveBrain(brain) {
|
|
232
|
+
if (brain.effort != null
|
|
233
|
+
&& (typeof brain.effort !== 'string' || !EFFORT_LEVELS.includes(brain.effort)))
|
|
234
|
+
throw new Error(`Claude Code Brain effort must be one of: ${EFFORT_LEVELS.join(', ')}`);
|
|
235
|
+
return {
|
|
236
|
+
model: brain.model,
|
|
237
|
+
harnessOptions: { ...(brain.harnessOptions ?? {}), ...(brain.effort ? { effort: brain.effort } : {}) },
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
modelEnvironmentVariable: () => 'ANTHROPIC_MODEL',
|
|
231
241
|
prepareLaunch(role, prep) {
|
|
232
242
|
const configured = role.session_options?.acp?.command;
|
|
233
243
|
const argv = Array.isArray(configured)
|
|
@@ -3,6 +3,8 @@ import type { AcpMcpServer, SessionPrep } from './types.js';
|
|
|
3
3
|
import type { AgentSessionAdapter, AgentSessionStartOptions, PreparedAgentSessionLaunch } from './agent-session.js';
|
|
4
4
|
import { type AcpSessionTransport } from './acp-session-transport.js';
|
|
5
5
|
export interface CodexSessionStrategy {
|
|
6
|
+
resolveBrain: AgentSessionAdapter['resolveBrain'];
|
|
7
|
+
modelEnvironmentVariable: AgentSessionAdapter['modelEnvironmentVariable'];
|
|
6
8
|
prepareLaunch(role: ResolvedRole, prep: SessionPrep): PreparedAgentSessionLaunch;
|
|
7
9
|
permissionModeId(role: ResolvedRole): string | undefined;
|
|
8
10
|
mcpServers(role: ResolvedRole): AcpMcpServer[] | undefined;
|
|
@@ -13,6 +15,8 @@ export declare class CodexAgentSessionAdapter implements AgentSessionAdapter {
|
|
|
13
15
|
private readonly strategy;
|
|
14
16
|
private readonly transport;
|
|
15
17
|
constructor(strategy: CodexSessionStrategy, transport?: AcpSessionTransport);
|
|
18
|
+
resolveBrain(brain: Parameters<AgentSessionAdapter['resolveBrain']>[0]): import("./agent-session.js").ResolvedBrainSelection;
|
|
19
|
+
modelEnvironmentVariable(): string | undefined;
|
|
16
20
|
prepareLaunch(role: ResolvedRole, prep: SessionPrep): PreparedAgentSessionLaunch;
|
|
17
21
|
start(options: AgentSessionStartOptions): Promise<import("../index.js").AgentSession>;
|
|
18
22
|
}
|
|
@@ -8,6 +8,10 @@ export class CodexAgentSessionAdapter {
|
|
|
8
8
|
this.strategy = strategy;
|
|
9
9
|
this.transport = transport;
|
|
10
10
|
}
|
|
11
|
+
resolveBrain(brain) {
|
|
12
|
+
return this.strategy.resolveBrain(brain);
|
|
13
|
+
}
|
|
14
|
+
modelEnvironmentVariable() { return this.strategy.modelEnvironmentVariable(); }
|
|
11
15
|
prepareLaunch(role, prep) {
|
|
12
16
|
return this.strategy.prepareLaunch(role, prep);
|
|
13
17
|
}
|
package/dist/harness/codex.js
CHANGED
|
@@ -251,6 +251,20 @@ export function makeCodexAdapter(exec = realExec, transport) {
|
|
|
251
251
|
return {
|
|
252
252
|
id: 'codex',
|
|
253
253
|
agentSession: new CodexAgentSessionAdapter({
|
|
254
|
+
resolveBrain(brain) {
|
|
255
|
+
const levels = ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
|
|
256
|
+
if (brain.effort != null
|
|
257
|
+
&& (typeof brain.effort !== 'string' || !levels.includes(brain.effort)))
|
|
258
|
+
throw new Error(`Codex Brain effort must be one of: ${levels.join(', ')}`);
|
|
259
|
+
const harnessOptions = { ...(brain.harnessOptions ?? {}) };
|
|
260
|
+
if (brain.effort)
|
|
261
|
+
harnessOptions.config = {
|
|
262
|
+
...(harnessOptions.config ?? {}),
|
|
263
|
+
model_reasoning_effort: brain.effort,
|
|
264
|
+
};
|
|
265
|
+
return { model: brain.model, harnessOptions };
|
|
266
|
+
},
|
|
267
|
+
modelEnvironmentVariable: () => undefined,
|
|
254
268
|
prepareLaunch(role, prep) {
|
|
255
269
|
const launch = codexAgentLaunch(role, prep);
|
|
256
270
|
const { permissionMetadataSource, ...neutral } = launch;
|
package/dist/harness/registry.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
const adapters = new Map();
|
|
2
2
|
export function registerAdapter(a) {
|
|
3
|
+
if (typeof a.agentSession?.resolveBrain !== 'function')
|
|
4
|
+
throw new Error(`harness adapter '${a.id}' must implement agentSession.resolveBrain(): translate neutral model/effort`);
|
|
5
|
+
if (typeof a.agentSession?.modelEnvironmentVariable !== 'function')
|
|
6
|
+
throw new Error(`harness adapter '${a.id}' must implement agentSession.modelEnvironmentVariable()`);
|
|
3
7
|
// Enforced here rather than left to the type system: an adapter that silently
|
|
4
8
|
// omits the capability is how the neutral-permission warnings ended up with no
|
|
5
9
|
// caller and no reader.
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { effectivePermissionMode } from './permissions.js';
|
|
|
13
13
|
export { pickBackend } from './supervisor/index.js';
|
|
14
14
|
export type { SupervisorBackend } from './supervisor/types.js';
|
|
15
15
|
export { up, down, restartRoles, rmRole, applyRole } from './ops.js';
|
|
16
|
-
export { spawnPermanent, spawnTemp,
|
|
16
|
+
export { spawnPermanent, spawnTemp, agentDefinitionFromSpawn, validateSpawnOpts, } from './spawn.js';
|
|
17
17
|
export { RoleRepository } from './application/role-repository.js';
|
|
18
18
|
export { FleetQueryService } from './application/fleet-query-service.js';
|
|
19
19
|
export { AcpRoleSessionAdapter } from './application/session-control.js';
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { generateBriefing } from './briefing.js';
|
|
|
9
9
|
export { effectivePermissionMode } from './permissions.js';
|
|
10
10
|
export { pickBackend } from './supervisor/index.js';
|
|
11
11
|
export { up, down, restartRoles, rmRole, applyRole } from './ops.js';
|
|
12
|
-
export { spawnPermanent, spawnTemp,
|
|
12
|
+
export { spawnPermanent, spawnTemp, agentDefinitionFromSpawn, validateSpawnOpts, } from './spawn.js';
|
|
13
13
|
export { RoleRepository } from './application/role-repository.js';
|
|
14
14
|
export { FleetQueryService } from './application/fleet-query-service.js';
|
|
15
15
|
export { AcpRoleSessionAdapter } from './application/session-control.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const INIT_COMPLETION_GUIDANCE = "\nNext:\n cp \"$(npm root -g)/@ours.network/fleet/examples/fleet.yaml\" ~/fleet.yaml\n cp -R \"$(npm root -g)/@ours.network/fleet/examples/fleet\" ~/fleet\n edit ~/fleet.yaml and ~/fleet/{agents,roles,brains}/*.yaml, then: ours-fleet up";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const INIT_COMPLETION_GUIDANCE = `
|
|
2
|
+
Next:
|
|
3
|
+
cp "$(npm root -g)/@ours.network/fleet/examples/fleet.yaml" ~/fleet.yaml
|
|
4
|
+
cp -R "$(npm root -g)/@ours.network/fleet/examples/fleet" ~/fleet
|
|
5
|
+
edit ~/fleet.yaml and ~/fleet/{agents,roles,brains}/*.yaml, then: ours-fleet up`;
|
package/dist/model-env.d.ts
CHANGED
|
@@ -10,14 +10,13 @@ import type { ResolvedRole } from './config.js';
|
|
|
10
10
|
* ACP role, and a fleet-wide `defaults.env.ANTHROPIC_MODEL` silently outranked
|
|
11
11
|
* an explicitly requested one.
|
|
12
12
|
*/
|
|
13
|
-
export declare const MODEL_ENV_BY_HARNESS: Readonly<Record<string, string>>;
|
|
14
13
|
/** The model-pin variable for a harness, or undefined if it pins no model by env. */
|
|
15
14
|
export declare function modelEnvVar(harness: string | undefined): string | undefined;
|
|
16
15
|
export interface RoleModelEnvInput {
|
|
17
16
|
harness: string;
|
|
18
17
|
/** Already resolved by `resolveRoleModel` — may come from the fleet default. */
|
|
19
18
|
model: string | undefined;
|
|
20
|
-
/** True when the
|
|
19
|
+
/** True when the resolved Brain named a model, including `model: null`. */
|
|
21
20
|
modelWasExplicit: boolean;
|
|
22
21
|
defaultsEnv?: Record<string, string>;
|
|
23
22
|
roleEnv?: Record<string, string>;
|
|
@@ -36,7 +35,7 @@ export interface RoleModelEnv {
|
|
|
36
35
|
* never disagree.
|
|
37
36
|
*
|
|
38
37
|
* Precedence, highest first:
|
|
39
|
-
* 1. an explicit `model:`
|
|
38
|
+
* 1. an explicit `model:` on the resolved Brain
|
|
40
39
|
* 2. the role's own `env:` pin
|
|
41
40
|
* 3. the fleet `defaults.model`
|
|
42
41
|
* 4. the fleet `defaults.env` pin
|
package/dist/model-env.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getAdapter } from './harness/registry.js';
|
|
1
2
|
/**
|
|
2
3
|
* Which environment variable a harness reads to pin the model it RUNS.
|
|
3
4
|
*
|
|
@@ -9,19 +10,16 @@
|
|
|
9
10
|
* ACP role, and a fleet-wide `defaults.env.ANTHROPIC_MODEL` silently outranked
|
|
10
11
|
* an explicitly requested one.
|
|
11
12
|
*/
|
|
12
|
-
export const MODEL_ENV_BY_HARNESS = {
|
|
13
|
-
'claude-code': 'ANTHROPIC_MODEL',
|
|
14
|
-
};
|
|
15
13
|
/** The model-pin variable for a harness, or undefined if it pins no model by env. */
|
|
16
14
|
export function modelEnvVar(harness) {
|
|
17
|
-
return harness === undefined ? undefined :
|
|
15
|
+
return harness === undefined ? undefined : getAdapter(harness).agentSession.modelEnvironmentVariable();
|
|
18
16
|
}
|
|
19
17
|
/**
|
|
20
18
|
* Resolve a role's environment and its runtime model TOGETHER, so the two can
|
|
21
19
|
* never disagree.
|
|
22
20
|
*
|
|
23
21
|
* Precedence, highest first:
|
|
24
|
-
* 1. an explicit `model:`
|
|
22
|
+
* 1. an explicit `model:` on the resolved Brain
|
|
25
23
|
* 2. the role's own `env:` pin
|
|
26
24
|
* 3. the fleet `defaults.model`
|
|
27
25
|
* 4. the fleet `defaults.env` pin
|
package/dist/ops.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, unlinkSync, } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { randomUUID, createHash } from 'node:crypto';
|
|
4
|
-
import { agentDir,
|
|
4
|
+
import { agentDir, watchdogsRoot } from './paths.js';
|
|
5
5
|
import { findRole } from './config.js';
|
|
6
6
|
import { getAdapter } from './harness/registry.js';
|
|
7
7
|
import { generateBriefing } from './briefing.js';
|
|
@@ -9,6 +9,7 @@ import { resetRestartLedger } from './runner.js';
|
|
|
9
9
|
import { archiveTempState, stopTempSupervisor, tempSupervisorLiveness, } from './temp-lifecycle.js';
|
|
10
10
|
import { realExec } from './exec.js';
|
|
11
11
|
import { daemonIdentityProvisioner, reconcilePermanentRoleIdentities, } from './creation.js';
|
|
12
|
+
import { provesGeneratedAgentSource } from './generated-agent-source.js';
|
|
12
13
|
// Launch staggering now lives at the harness-launch point (the runner's start
|
|
13
14
|
// gate, driven by `start_stagger_ms`), so it covers systemd host-boot too — not
|
|
14
15
|
// just the `up`/`restart` command loop below. The old in-loop FLEET_START_STAGGER
|
|
@@ -264,9 +265,10 @@ export async function rmRole(cfg, name, deps) {
|
|
|
264
265
|
return;
|
|
265
266
|
}
|
|
266
267
|
const role = findRole(cfg, name);
|
|
268
|
+
const generatedSource = provesGeneratedAgentSource(agentDir(name), cfg.files[0], role.sourceFile);
|
|
267
269
|
await deps.backend.uninstall(name);
|
|
268
270
|
rmSync(agentDir(name), { recursive: true, force: true });
|
|
269
|
-
if (role.sourceFile
|
|
271
|
+
if (generatedSource && existsSync(role.sourceFile)) {
|
|
270
272
|
unlinkSync(role.sourceFile);
|
|
271
273
|
deps.log(`removed ${role.sourceFile}`);
|
|
272
274
|
}
|
|
@@ -630,7 +630,8 @@ export const ownerCommands = [
|
|
|
630
630
|
...(t.builtin ? ['Source: built-in'] : []),
|
|
631
631
|
...(t.contract ? [`Contract: ${t.contract}`] : []),
|
|
632
632
|
'Members:',
|
|
633
|
-
...t.members.map(m => ` ${m.slot} (${m.role}) ×${m.count} →
|
|
633
|
+
...t.members.map(m => ` ${m.slot} (${m.role}) ×${m.count} → `
|
|
634
|
+
+ `${'ref' in m.agent ? `Agent ref: ${m.agent.ref}` : 'inline Agent'}`),
|
|
634
635
|
];
|
|
635
636
|
await ctx.reply(lines.join('\n'));
|
|
636
637
|
};
|
package/dist/resolved-plan.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FleetConfig, ResolvedRole } from './config.js';
|
|
2
|
-
export declare const RESOLVED_PLAN_SCHEMA_VERSION =
|
|
2
|
+
export declare const RESOLVED_PLAN_SCHEMA_VERSION = 2;
|
|
3
|
+
export declare const redactSensitive: (value: unknown, key?: string) => unknown;
|
|
3
4
|
/** Stable, secret-safe machine contract consumed by config JSON and spawn dry-run. */
|
|
4
5
|
export declare function resolvedPlan(cfg: FleetConfig): Record<string, unknown>;
|
|
5
|
-
export declare function resolvedRolePlan(role: ResolvedRole): Record<string, unknown>;
|
|
6
|
+
export declare function resolvedRolePlan(role: ResolvedRole, manifestDir?: string): Record<string, unknown>;
|
package/dist/resolved-plan.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { analyzeRolePermissions } from './permissions.js';
|
|
2
|
-
|
|
2
|
+
import { dirname, relative } from 'node:path';
|
|
3
|
+
import { isSensitiveConfigKey } from './sensitive-config.js';
|
|
4
|
+
export const RESOLVED_PLAN_SCHEMA_VERSION = 2;
|
|
3
5
|
const sortedObject = (value) => Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)));
|
|
4
|
-
const redactSensitive = (value, key = '') => {
|
|
5
|
-
if (
|
|
6
|
+
export const redactSensitive = (value, key = '') => {
|
|
7
|
+
if (isSensitiveConfigKey(key))
|
|
6
8
|
return '<redacted>';
|
|
7
9
|
if (Array.isArray(value))
|
|
8
10
|
return value.map(item => redactSensitive(item));
|
|
@@ -14,12 +16,17 @@ const redactSensitive = (value, key = '') => {
|
|
|
14
16
|
};
|
|
15
17
|
/** Stable, secret-safe machine contract consumed by config JSON and spawn dry-run. */
|
|
16
18
|
export function resolvedPlan(cfg) {
|
|
19
|
+
const manifestDir = dirname(cfg.files[0] ?? '.');
|
|
17
20
|
return {
|
|
18
21
|
schemaVersion: RESOLVED_PLAN_SCHEMA_VERSION,
|
|
22
|
+
configMode: cfg.configMode ?? 'split-v2',
|
|
19
23
|
sourceFiles: [...cfg.files],
|
|
24
|
+
sourceDocuments: (cfg.sourceDocuments ?? []).map(document => ({
|
|
25
|
+
...document, path: relative(manifestDir, document.path) || document.path,
|
|
26
|
+
})),
|
|
20
27
|
startStaggerMs: cfg.startStaggerMs,
|
|
21
28
|
diagnostics: cfg.diagnostics.map(diagnostic => ({ ...diagnostic })),
|
|
22
|
-
roles: cfg.roles.map(resolvedRolePlan),
|
|
29
|
+
roles: cfg.roles.map(role => resolvedRolePlan(role, manifestDir)),
|
|
23
30
|
loops: cfg.loops.map(loop => sortedObject({
|
|
24
31
|
name: loop.name, selectors: [...loop.selectors], roles: [...loop.roleNames],
|
|
25
32
|
intervalMs: loop.intervalMs, initialDelayMs: loop.initialDelayMs, jitterMs: loop.jitterMs,
|
|
@@ -36,16 +43,25 @@ export function resolvedPlan(cfg) {
|
|
|
36
43
|
})),
|
|
37
44
|
};
|
|
38
45
|
}
|
|
39
|
-
export function resolvedRolePlan(role) {
|
|
46
|
+
export function resolvedRolePlan(role, manifestDir = dirname(role.sourceFile)) {
|
|
40
47
|
const analysis = analyzeRolePermissions(role);
|
|
41
48
|
return {
|
|
42
49
|
name: role.name,
|
|
43
50
|
sourceFile: role.sourceFile,
|
|
51
|
+
references: Object.values(role.provenance ?? {}).flatMap(item => item.viaReference ? [item.viaReference] : [])
|
|
52
|
+
.filter((item, index, all) => all.findIndex(other => JSON.stringify(other) === JSON.stringify(item)) === index)
|
|
53
|
+
.sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id)
|
|
54
|
+
|| a.sourcePointer.localeCompare(b.sourcePointer)),
|
|
55
|
+
provenance: sortedObject(Object.fromEntries(Object.entries(role.provenance ?? {}).map(([field, item]) => [
|
|
56
|
+
field, { ...item, sourceFile: item.sourceKind === 'built-in'
|
|
57
|
+
? item.sourceFile : relative(manifestDir, item.sourceFile) || item.sourceFile },
|
|
58
|
+
]))),
|
|
44
59
|
identity: role.identity,
|
|
45
60
|
harness: role.harness,
|
|
46
61
|
session: role.session,
|
|
47
62
|
sessionOptions: role.session_options ?? null,
|
|
48
63
|
model: role.model ?? null,
|
|
64
|
+
effort: role.effort ?? null,
|
|
49
65
|
modelChain: role.model_chain ?? null,
|
|
50
66
|
cwd: role.cwd ?? null,
|
|
51
67
|
coordinator: role.coordinator ?? null,
|
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -409,7 +409,8 @@ export function registerTemplateCommands(parent, cOpt) {
|
|
|
409
409
|
console.log(`\nContract:\n${t.contract}`);
|
|
410
410
|
console.log(`\nMembers:`);
|
|
411
411
|
for (const m of t.members)
|
|
412
|
-
console.log(` ${m.slot}: ${m.count}× ${m.role} (
|
|
412
|
+
console.log(` ${m.slot}: ${m.count}× ${m.role} (`
|
|
413
|
+
+ `${'ref' in m.agent ? `agent ref: ${m.agent.ref}` : 'inline Agent'})`);
|
|
413
414
|
console.log(`\nContent hash: ${t.content_hash}`);
|
|
414
415
|
}
|
|
415
416
|
catch (e) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { ROOMS_KEYS as RK, ROOMS_OWNER_KEYS as ROK, ROOMS_COWORK_KEYS as RCK, ROOMS_DEFAULTS_KEYS as RDK, TASKS_KEYS as TK, TEMPLATE_KEYS as TPK, TEMPLATE_MEMBER_KEYS as TMK,
|
|
3
|
+
import { ROOMS_KEYS as RK, ROOMS_OWNER_KEYS as ROK, ROOMS_COWORK_KEYS as RCK, ROOMS_DEFAULTS_KEYS as RDK, TASKS_KEYS as TK, TEMPLATE_KEYS as TPK, TEMPLATE_MEMBER_KEYS as TMK, } from './types.js';
|
|
4
4
|
import { BUILTIN_TEMPLATES } from './templates.js';
|
|
5
|
+
import { isSensitiveConfigKey } from '../sensitive-config.js';
|
|
5
6
|
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
6
7
|
const CID_RE = /^[0-9a-fA-F]{64}$/;
|
|
7
8
|
export class RoomsTasksConfigError extends Error {
|
|
@@ -136,19 +137,33 @@ export function validateRoomTemplatesConfig(raw, path) {
|
|
|
136
137
|
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].role: required string`);
|
|
137
138
|
if (!Number.isInteger(m.count) || m.count < 1)
|
|
138
139
|
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].count: required positive integer`);
|
|
139
|
-
if (!m.
|
|
140
|
-
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
if (!isPlainObject(m.agent))
|
|
141
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: required mapping`);
|
|
142
|
+
const agentKeys = Object.keys(m.agent);
|
|
143
|
+
const isRef = agentKeys.length === 1 && typeof m.agent.ref === 'string';
|
|
144
|
+
const isDefinition = agentKeys.includes('role') && agentKeys.includes('brain');
|
|
145
|
+
if (!isRef && !isDefinition)
|
|
146
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: must be {ref} or canonical {role, brain, ...}`);
|
|
147
|
+
const containsSensitive = (value) => Array.isArray(value)
|
|
148
|
+
? value.some(containsSensitive)
|
|
149
|
+
: Boolean(value && typeof value === 'object' && Object.entries(value)
|
|
150
|
+
.some(([key, child]) => isSensitiveConfigKey(key) || containsSensitive(child)));
|
|
151
|
+
if (isDefinition && containsSensitive(m.agent))
|
|
152
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: inline sensitive configuration cannot be persisted; use references`);
|
|
153
|
+
if (isDefinition) {
|
|
154
|
+
const definition = m.agent;
|
|
155
|
+
const brain = isPlainObject(definition.brain) && isPlainObject(definition.brain.inline)
|
|
156
|
+
? definition.brain.inline : undefined;
|
|
157
|
+
if (definition.env !== undefined || definition.owner_channel !== undefined
|
|
158
|
+
|| definition.auth_proxy !== undefined || brain?.harness_options !== undefined
|
|
159
|
+
|| brain?.session_options !== undefined)
|
|
160
|
+
throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: secret-capable inline fields cannot be persisted; use references`);
|
|
145
161
|
}
|
|
146
162
|
return {
|
|
147
163
|
slot: m.slot,
|
|
148
164
|
role: m.role,
|
|
149
165
|
count: m.count,
|
|
150
|
-
|
|
151
|
-
overrides: m.overrides,
|
|
166
|
+
agent: structuredClone(m.agent),
|
|
152
167
|
};
|
|
153
168
|
});
|
|
154
169
|
let room;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
3
3
|
import { parse } from 'yaml';
|
|
4
4
|
import { advanceSaga, setSagaError, updateMemberSeats, updateMemberStartup, activateRoom, getRoomRecord, } from './room-state.js';
|
|
5
5
|
import { activateTask, updateTaskMembers, blockTask, unblockTask, getTask, } from './task-state.js';
|
|
6
6
|
import { spawnTemp } from '../spawn.js';
|
|
7
|
-
import { findRole } from '../config.js';
|
|
7
|
+
import { findRole, loadConfig } from '../config.js';
|
|
8
8
|
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from '../fleet-proxy.js';
|
|
9
9
|
import { controlRequest } from '../session/control.js';
|
|
10
10
|
import { SessionControlError } from '../session/types.js';
|
|
@@ -21,6 +21,26 @@ export function getBinPath() {
|
|
|
21
21
|
return process.argv[1];
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
function canonicalJson(value) {
|
|
25
|
+
if (Array.isArray(value))
|
|
26
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
27
|
+
if (value && typeof value === 'object')
|
|
28
|
+
return `{${Object.entries(value)
|
|
29
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
30
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(',')}}`;
|
|
31
|
+
return JSON.stringify(value);
|
|
32
|
+
}
|
|
33
|
+
function launchDefinition(definition) {
|
|
34
|
+
const selectionEvidence = (selection) => 'ref' in selection ? { kind: 'ref', id: selection.ref } : { kind: 'inline' };
|
|
35
|
+
return {
|
|
36
|
+
projection: {
|
|
37
|
+
brain: selectionEvidence(definition.brain), role: selectionEvidence(definition.role),
|
|
38
|
+
operationalFields: Object.keys(definition)
|
|
39
|
+
.filter(key => key !== 'brain' && key !== 'role').sort(),
|
|
40
|
+
},
|
|
41
|
+
fingerprint: createHash('sha256').update(canonicalJson(definition)).digest('hex'),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
24
44
|
async function spawnRoomMember(options, binPath) {
|
|
25
45
|
const stateDir = process.env[FLEET_PROXY_STATE_DIR_ENV];
|
|
26
46
|
if (!stateDir)
|
|
@@ -52,28 +72,31 @@ function expandMembers(template, prefix) {
|
|
|
52
72
|
name: `${prefix}-${slot.slot}-${i}`,
|
|
53
73
|
slot: slot.slot,
|
|
54
74
|
coworkRole: slot.role,
|
|
55
|
-
|
|
56
|
-
overrides: slot.overrides,
|
|
75
|
+
agent: structuredClone(slot.agent),
|
|
57
76
|
});
|
|
58
77
|
}
|
|
59
78
|
}
|
|
60
79
|
return result;
|
|
61
80
|
}
|
|
62
81
|
function settingsFor(member, cfg) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
if (!('ref' in member.agent)) {
|
|
83
|
+
const role = member.agent.role;
|
|
84
|
+
if (cfg.files[0] && existsSync(cfg.files[0]) && statSync(cfg.files[0]).isFile())
|
|
85
|
+
loadConfig(cfg.files[0], { additionalAgent: {
|
|
86
|
+
id: `RoomPreflight${member.name.replace(/[^A-Za-z0-9_-]/g, '')}`.slice(0, 64),
|
|
87
|
+
definition: member.agent,
|
|
88
|
+
} });
|
|
89
|
+
return { definition: structuredClone(member.agent),
|
|
90
|
+
...('inline' in role && typeof role.inline.persona === 'string'
|
|
91
|
+
? { persona: role.inline.persona } : {}) };
|
|
66
92
|
}
|
|
67
|
-
|
|
68
|
-
const
|
|
93
|
+
const refRole = findRole(cfg, member.agent.ref);
|
|
94
|
+
const definition = cfg.agentDefinitions?.[member.agent.ref];
|
|
95
|
+
if (!definition)
|
|
96
|
+
throw new Error(`Agent '${member.agent.ref}' has no canonical definition`);
|
|
69
97
|
return {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
cwd: member.overrides?.cwd ?? refRole?.cwd,
|
|
73
|
-
persona: member.overrides?.persona ?? refRole?.persona,
|
|
74
|
-
approval: permissions?.approval,
|
|
75
|
-
filesystem: permissions?.filesystem,
|
|
76
|
-
unattended: permissions?.unattended,
|
|
98
|
+
definition: structuredClone(definition),
|
|
99
|
+
persona: refRole.persona,
|
|
77
100
|
};
|
|
78
101
|
}
|
|
79
102
|
function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid) {
|
|
@@ -104,13 +127,11 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
|
|
|
104
127
|
const role = parse(readFileSync(`${dir}/role.yaml`, 'utf8'));
|
|
105
128
|
const startup = role.roomMemberStartup;
|
|
106
129
|
return role.identity === member.name
|
|
107
|
-
&& typeof role.mission === 'string'
|
|
108
|
-
&& sha256Text(role.mission) === taskSha
|
|
109
130
|
&& startup?.room_id === roomId
|
|
110
131
|
&& startup.room_identity_cid === roomIdentityCid
|
|
111
132
|
&& startup.identity_name === member.name
|
|
112
133
|
&& startup.role === member.coworkRole
|
|
113
|
-
&& startup.task ===
|
|
134
|
+
&& sha256Text(startup.task ?? '') === taskSha
|
|
114
135
|
&& (expectedInviteId === undefined || startup.invite_id === expectedInviteId)
|
|
115
136
|
&& typeof startup.invite === 'string'
|
|
116
137
|
&& startup.invite.length > 0;
|
|
@@ -199,10 +220,13 @@ async function launchMember(input) {
|
|
|
199
220
|
let effectiveActionId = actionId;
|
|
200
221
|
const attempt = (seat.launch?.attempt ?? 0) + 1;
|
|
201
222
|
const taskSha = sha256Text(startup.task);
|
|
223
|
+
const effectiveAgentDefinition = structuredClone(settings.definition);
|
|
224
|
+
const { projection: agentDefinition, fingerprint: agentFingerprint } = launchDefinition(effectiveAgentDefinition);
|
|
202
225
|
const proxyCaller = process.env[FLEET_PROXY_STATE_DIR_ENV]
|
|
203
226
|
? process.env[FLEET_PROXY_CALLER_ENV] : undefined;
|
|
204
227
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
205
228
|
state: 'intent', attempt, action_id: actionId, mission_sha256: taskSha,
|
|
229
|
+
agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
|
|
206
230
|
...(proxyCaller ? { caller_role: proxyCaller } : {}),
|
|
207
231
|
updated_at: new Date().toISOString(),
|
|
208
232
|
} });
|
|
@@ -211,13 +235,7 @@ async function launchMember(input) {
|
|
|
211
235
|
name: member.name,
|
|
212
236
|
temp: true,
|
|
213
237
|
identity: member.name,
|
|
214
|
-
|
|
215
|
-
model: settings.model,
|
|
216
|
-
harness: settings.harness,
|
|
217
|
-
cwd: settings.cwd,
|
|
218
|
-
approval: settings.approval,
|
|
219
|
-
filesystem: settings.filesystem,
|
|
220
|
-
unattended: settings.unattended,
|
|
238
|
+
agentDefinition: settings.definition,
|
|
221
239
|
surface: 'agent',
|
|
222
240
|
creationActionId: actionId,
|
|
223
241
|
roomMemberStartup: startup,
|
|
@@ -227,7 +245,8 @@ async function launchMember(input) {
|
|
|
227
245
|
if (launched.creationActionId !== actionId) {
|
|
228
246
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
229
247
|
state: 'intent', attempt, action_id: launched.creationActionId,
|
|
230
|
-
mission_sha256: taskSha,
|
|
248
|
+
mission_sha256: taskSha, agent_definition: agentDefinition,
|
|
249
|
+
agent_fingerprint: agentFingerprint, updated_at: new Date().toISOString(),
|
|
231
250
|
...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
|
|
232
251
|
} });
|
|
233
252
|
}
|
|
@@ -237,6 +256,7 @@ async function launchMember(input) {
|
|
|
237
256
|
}
|
|
238
257
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
239
258
|
state: 'launched', attempt, action_id: launched.creationActionId, mission_sha256: taskSha,
|
|
259
|
+
agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
|
|
240
260
|
...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
|
|
241
261
|
launch_id: supervisor.launchId, updated_at: new Date().toISOString(),
|
|
242
262
|
} });
|
|
@@ -244,6 +264,7 @@ async function launchMember(input) {
|
|
|
244
264
|
catch (error) {
|
|
245
265
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
246
266
|
state: 'failed', attempt, action_id: effectiveActionId, mission_sha256: taskSha,
|
|
267
|
+
agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
|
|
247
268
|
...(proxyCaller ? { caller_role: proxyCaller } : {}),
|
|
248
269
|
updated_at: new Date().toISOString(),
|
|
249
270
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -289,6 +310,8 @@ export async function provisionMembers(input) {
|
|
|
289
310
|
const { cfg, cowork, roomId, taskId, template } = input;
|
|
290
311
|
const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
|
|
291
312
|
const members = expandMembers(template, prefix);
|
|
313
|
+
// Resolve every Agent before persisting launch intent or touching Cowork membership.
|
|
314
|
+
const settings = new Map(members.map(member => [member.name, settingsFor(member, cfg)]));
|
|
292
315
|
const existing = getRoomRecord(roomId);
|
|
293
316
|
if (!existing?.room_identity_cid)
|
|
294
317
|
throw new Error(`room ${roomId} has no pinned room identity CID`);
|
|
@@ -299,15 +322,27 @@ export async function provisionMembers(input) {
|
|
|
299
322
|
&& members.every(member => persistedNames.has(member.name));
|
|
300
323
|
if (!resuming) {
|
|
301
324
|
advanceSaga(roomId, 'create_members', 3);
|
|
302
|
-
updateMemberSeats(roomId, members.map(member =>
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
325
|
+
updateMemberSeats(roomId, members.map(member => {
|
|
326
|
+
const evidence = launchDefinition(settings.get(member.name).definition);
|
|
327
|
+
return ({
|
|
328
|
+
role_name: member.name,
|
|
329
|
+
slot: member.slot,
|
|
330
|
+
cowork_role: member.coworkRole,
|
|
331
|
+
seat_state: 'pending',
|
|
332
|
+
launch: { state: 'pending', attempt: 0,
|
|
333
|
+
agent_definition: evidence.projection, agent_fingerprint: evidence.fingerprint,
|
|
334
|
+
updated_at: new Date().toISOString() },
|
|
335
|
+
});
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
for (const member of members) {
|
|
340
|
+
const seat = existing.member_seats.find(candidate => candidate.role_name === member.name);
|
|
341
|
+
const evidence = launchDefinition(settings.get(member.name).definition);
|
|
342
|
+
if (!seat.launch?.agent_fingerprint || seat.launch.agent_fingerprint !== evidence.fingerprint)
|
|
343
|
+
throw new Error(`Agent definition drift for ${member.name}; durable launch intent does not match current configuration`);
|
|
344
|
+
}
|
|
309
345
|
}
|
|
310
|
-
const settings = new Map(members.map(member => [member.name, settingsFor(member, cfg)]));
|
|
311
346
|
const tasks = new Map(members.map(member => [member.name, roomTask(input, member, settings.get(member.name), members, roomIdentityCid, ownerSeatCid)]));
|
|
312
347
|
const policy = {
|
|
313
348
|
timeoutMs: input.startupWait?.timeoutMs ?? 60_000,
|
|
@@ -12,9 +12,9 @@ const TEAM = {
|
|
|
12
12
|
'Completion requires tester sign-off.',
|
|
13
13
|
].join('\n'),
|
|
14
14
|
members: [
|
|
15
|
-
{ slot: 'architect', role: 'Architect', count: 1,
|
|
16
|
-
{ slot: 'developer', role: 'Developer', count: 1,
|
|
17
|
-
{ slot: 'tester', role: 'Tester', count: 1,
|
|
15
|
+
{ slot: 'architect', role: 'Architect', count: 1, agent: { ref: 'Architect' } },
|
|
16
|
+
{ slot: 'developer', role: 'Developer', count: 1, agent: { ref: 'Developer' } },
|
|
17
|
+
{ slot: 'tester', role: 'Tester', count: 1, agent: { ref: 'Tester' } },
|
|
18
18
|
],
|
|
19
19
|
};
|
|
20
20
|
const PAIR = {
|
|
@@ -30,8 +30,8 @@ const PAIR = {
|
|
|
30
30
|
'Completion requires joint sign-off.',
|
|
31
31
|
].join('\n'),
|
|
32
32
|
members: [
|
|
33
|
-
{ slot: 'secretary', role: 'Secretary', count: 1,
|
|
34
|
-
{ slot: 'critic', role: 'Critic', count: 1,
|
|
33
|
+
{ slot: 'secretary', role: 'Secretary', count: 1, agent: { ref: 'Secretary' } },
|
|
34
|
+
{ slot: 'critic', role: 'Critic', count: 1, agent: { ref: 'Critic' } },
|
|
35
35
|
],
|
|
36
36
|
};
|
|
37
37
|
const SINGLE = {
|
|
@@ -45,7 +45,7 @@ const SINGLE = {
|
|
|
45
45
|
'Owner reviews and approves completion.',
|
|
46
46
|
].join('\n'),
|
|
47
47
|
members: [
|
|
48
|
-
{ slot: 'agent', role: 'Agent', count: 1,
|
|
48
|
+
{ slot: 'agent', role: 'Agent', count: 1, agent: { ref: 'Agent' } },
|
|
49
49
|
],
|
|
50
50
|
};
|
|
51
51
|
export const BUILTIN_TEMPLATES = [TEAM, PAIR, SINGLE];
|