@ours.network/fleet 1.1.0-nightly.20 → 1.1.0-nightly.21

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/dist/spawn.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawn as spawnChild } from 'node:child_process';
2
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { parse, stringify } from 'yaml';
5
5
  import { agentDir, defaultConfigPath } from './paths.js';
@@ -22,9 +22,12 @@ export function agentDefinitionFromSpawn(o) {
22
22
  if (o.agentDefinition) {
23
23
  if (o.brain !== undefined || o.role !== undefined || o.cwd !== undefined
24
24
  || o.coordinator !== undefined || o.approval !== undefined || o.filesystem !== undefined
25
- || o.unattended !== undefined || o.isolationFile !== undefined || o.monitorConfig !== undefined)
25
+ || o.unattended !== undefined || o.isolationFile !== undefined || o.monitorConfig !== undefined
26
+ || o.loopsFile !== undefined)
26
27
  throw new Error('canonical agentDefinition conflicts with separate Agent fields');
27
28
  const definition = structuredClone(o.agentDefinition);
29
+ if (o.noLoops === true && definition.loops !== undefined)
30
+ throw new Error('canonical agentDefinition loops conflict with explicit no-loops policy');
28
31
  if (o.identity)
29
32
  definition.identity = o.identity;
30
33
  return definition;
@@ -46,12 +49,21 @@ export function agentDefinitionFromSpawn(o) {
46
49
  ...(permissions ? { permissions } : {}),
47
50
  ...(o.isolationFile ? { isolation: readIsolationFile(o.isolationFile) } : {}),
48
51
  ...(o.monitorConfig ? { monitor: structuredClone(o.monitorConfig) } : {}),
52
+ ...(o.loopsFile ? { loops: readLoopsFile(o.loopsFile) } : {}),
49
53
  };
50
54
  }
51
55
  function resolvedSpawn(o) {
52
56
  const definition = agentDefinitionFromSpawn(o);
53
- const cfg = loadConfig(o.configPath, { additionalAgent: { id: o.name, definition } });
54
- return { definition, role: findRole(cfg, o.name) };
57
+ const cfg = loadConfig(o.configPath, {
58
+ additionalAgent: { id: o.name, definition, temporary: o.temp === true },
59
+ });
60
+ const role = findRole(cfg, o.name);
61
+ if (o.temp)
62
+ role.temporaryLoopSource = o.loopSource
63
+ ?? (role.temporaryLoops?.length ? 'agent-template' : 'omitted');
64
+ if (o.noLoops === true)
65
+ role.temporaryLoops = [];
66
+ return { definition, role };
55
67
  }
56
68
  /**
57
69
  * Read and validate an `--isolation-file`. The file is the existing
@@ -78,7 +90,37 @@ export function readIsolationFile(path) {
78
90
  throw new Error(`--isolation-file ${path}: ${problems.join('; ')}`);
79
91
  return cfg;
80
92
  }
93
+ /** Read a private canonical temporary-loop override before any creation side effect. */
94
+ export function readLoopsFile(path) {
95
+ const stat = lstatSync(path);
96
+ const uid = process.getuid?.();
97
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_000_000
98
+ || (uid !== undefined && stat.uid !== uid) || (stat.mode & 0o777) !== 0o600)
99
+ throw new Error(`${path}: loops file must be an owner-only regular file no larger than 1 MB`);
100
+ let raw;
101
+ try {
102
+ raw = parse(readFileSync(path, 'utf8'));
103
+ }
104
+ catch (error) {
105
+ throw new Error(`--loops-file ${path}: ${error.message}`);
106
+ }
107
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)
108
+ || !Object.hasOwn(raw, 'loops')
109
+ || Object.keys(raw).some(key => key !== 'loops'))
110
+ throw new Error(`${path}: expected exactly a top-level loops: mapping`);
111
+ const loops = raw.loops;
112
+ if (!loops || typeof loops !== 'object' || Array.isArray(loops)
113
+ || !Object.keys(loops).length)
114
+ throw new Error(`${path}: loops must be a non-empty mapping; use --no-loops to disable loops`);
115
+ return structuredClone(loops);
116
+ }
81
117
  export function validateSpawnOpts(o) {
118
+ if (o.loopsFile && o.noLoops === true)
119
+ throw new Error('--loops-file and --no-loops are mutually exclusive');
120
+ if ((o.loopsFile || o.noLoops === true || o.agentDefinition?.loops !== undefined) && !o.temp)
121
+ throw new Error('temporary Agent loops require --temp');
122
+ if (o.loopsFile)
123
+ readLoopsFile(o.loopsFile);
82
124
  if (o.approval && !['ask', 'auto', 'allow', 'deny'].includes(o.approval))
83
125
  throw new Error(`invalid --approval '${o.approval}'; allowed: ask, auto, allow (deprecated alias: deny)`);
84
126
  if (o.filesystem && !['read-only', 'workspace', 'unrestricted'].includes(o.filesystem))
@@ -149,7 +191,7 @@ export function spawnDryRun(o) {
149
191
  * `env`, `bio`, `persona` and `harness_options` are deliberately absent: the
150
192
  * record exists to be read, and must not become a place credentials collect.
151
193
  */
152
- function provenanceSettings(o, defaults) {
194
+ function provenanceSettings(o, defaults, resolvedLoops = []) {
153
195
  const perms = (defaults.permissions ?? {});
154
196
  const callerDefaults = new Set(o.inheritedFromCaller ?? []);
155
197
  const tagged = (key, entry) => callerDefaults.has(key) ? { ...entry, source: 'caller-role' } : entry;
@@ -169,6 +211,16 @@ function provenanceSettings(o, defaults) {
169
211
  ? { value: 'declared via --isolation-file', source: 'cli' }
170
212
  : { value: defaults.isolation ? 'from fleet defaults' : undefined, source: defaults.isolation ? 'fleet-default' : 'built-in' },
171
213
  monitor: tagged('monitorConfig', provenanceOf(o.monitorConfig, defaults.monitor, { mode: 'fleet' })),
214
+ loops: {
215
+ value: o.noLoops ? { enabled: false, loops: [] }
216
+ : resolvedLoops.length ? resolvedLoops.map(loop => ({
217
+ name: loop.name, enabled: loop.enabled, intervalMs: loop.intervalMs,
218
+ initialDelayMs: loop.initialDelayMs, jitterMs: loop.jitterMs,
219
+ prompt: { bytes: loop.promptBytes, sha256: loop.promptHash },
220
+ })) : 'omitted (legacy no temporary loops)',
221
+ source: o.loopSource === 'agent-template' ? 'agent-template'
222
+ : o.loopsFile || o.noLoops || o.loopSource === 'cli' ? 'cli' : 'built-in',
223
+ },
172
224
  };
173
225
  }
174
226
  /** Permanent spawn: persist one bare Agent document under the selected v2 root. */
@@ -232,7 +284,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
232
284
  // launch still records how it was asked for.
233
285
  const provenance = buildProvenance({
234
286
  role: o.name, lifetime: 'permanent', fleetVersion: VERSION,
235
- settings: provenanceSettings(o, cfg.defaults),
287
+ settings: provenanceSettings(o, cfg.defaults, prepared.role.temporaryLoops),
236
288
  surface: o.surface, creationActionId: o.creationActionId, callerRole: o.callerRole,
237
289
  });
238
290
  mkdirSync(agentDir(o.name), { recursive: true });
@@ -293,14 +345,19 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
293
345
  }
294
346
  async function spawnTempInner(o, preparedRole, binPath, launch, tx, onStage) {
295
347
  const cfg = loadConfig(o.configPath);
348
+ const { temporaryLoops, ...launchRole } = preparedRole;
296
349
  const role = {
297
- ...preparedRole, sourceFile: '(temp)', loops: undefined, roomMemberStartup: o.roomMemberStartup,
350
+ ...launchRole, sourceFile: '(temp)',
351
+ ...(o.noLoops === true ? { loops: [] }
352
+ : temporaryLoops?.length ? { loops: temporaryLoops } : { loops: undefined }),
353
+ temporaryLoopSource: o.loopSource ?? (temporaryLoops?.length ? 'agent-template' : 'omitted'),
354
+ roomMemberStartup: o.roomMemberStartup,
298
355
  };
299
356
  onStage?.('writing_role');
300
357
  const dir = applyRole(role, { temp: true, identityGuarantee: 'unverified' });
301
358
  const provenance = buildProvenance({
302
359
  role: o.name, lifetime: 'temporary', fleetVersion: VERSION,
303
- settings: provenanceSettings(o, cfg.defaults),
360
+ settings: provenanceSettings(o, cfg.defaults, preparedRole.temporaryLoops),
304
361
  surface: o.surface, creationActionId: o.creationActionId, callerRole: o.callerRole,
305
362
  });
306
363
  writeProvenance(dir, provenance);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.1.0-nightly.20",
3
+ "version": "1.1.0-nightly.21",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",