@ours.network/fleet 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -6,7 +6,7 @@ FSL-1.1-Apache-2.0
6
6
 
7
7
  ## Notice
8
8
 
9
- Copyright 2026 ours.network contributors
9
+ Copyright 2026 Adapt Framework Solutions Ltd
10
10
 
11
11
  ## Terms and Conditions
12
12
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ours-fleet
2
2
 
3
- **Run a fleet of persistent, identity-bound AI agents — across different agent
3
+ **Run a fleet of persistent, securely isolated, identity-bound AI agents — across different agent
4
4
  harnesses — from one declarative file.**
5
5
 
6
6
  ## What is this?
@@ -178,12 +178,51 @@ roles:
178
178
  # mem_palace: false # claude-code: disable memory plugin
179
179
  # permission_mode: dontAsk # claude-code: launch permission mode —
180
180
  # one of default | acceptEdits | plan | dontAsk | bypassPermissions
181
+ isolation: # OS-level sandbox (additive; omit = today's behavior)
182
+ backend: auto # auto | bubblewrap | podman | none (default auto)
183
+ on_unavailable: warn # warn (un-isolated + marker) | strict (refuse) (default warn)
184
+ network: broker # broker | deny | allow | allowlist (default broker)
185
+ fs: { read: [/opt/toolchains], write: [] } # extra binds (state dir + cwd always included)
186
+ resources: { mem: 2G, cpu: "1.5", pids: 512 }
187
+ secrets: ["/host/tok:/run/secrets/tok"] # host:container, mounted read-only
181
188
  ```
182
189
 
183
190
  Merge order: `fleet.yaml` ← `fleet.d/*.yaml`; a duplicate role name is a hard
184
191
  error naming both files. Identities and roles are decoupled — removing a role
185
192
  never deletes an identity.
186
193
 
194
+ ## Agent isolation
195
+
196
+ Each role can be sandboxed at the environment level via an `isolation:` block —
197
+ **fully additive: a role with no block behaves exactly as before.** The agent's
198
+ tmux-pane process is wrapped in [bubblewrap](https://github.com/containers/bubblewrap)
199
+ (rootless, no setuid), resource-limited by `systemd-run --user --scope`.
200
+
201
+ An empty `isolation: {}` gives a sensible default posture: filesystem-confined to
202
+ the state dir + `cwd`, the ours key store / other agents' state / `~/.ssh` / `~/.aws`
203
+ all invisible, ours messaging still works, no hard resource caps.
204
+
205
+ - **`backend`** — `auto` (bubblewrap if usable, else degrade per `on_unavailable`),
206
+ or force `bubblewrap` / `none`. (`podman` is planned.)
207
+ - **`on_unavailable`** — `warn` (default, fail-open: run un-isolated, log, and drop a
208
+ `.isolation-degraded` marker in the state dir) or `strict` (fail closed: refuse to launch).
209
+ - **`network`** — `broker` (default; ours messaging works), `deny` (no network),
210
+ `allow` (unrestricted), `allowlist` (planned). *Current status:* `deny` fully
211
+ unshares the network; `broker` keeps host networking so the loopback ours daemon
212
+ stays reachable — full broker egress-hardening is a follow-up.
213
+ - **`fs.read` / `fs.write`** — extra read-only / read-write binds on top of the durable set.
214
+ - **`resources`** — `mem` (→ `MemoryMax` + `MemorySwapMax=0`, a hard OOM bound),
215
+ `cpu` cores (→ `CPUQuota`), `pids` (→ `TasksMax`). CPU degrades to a warning if the
216
+ cpu cgroup controller isn't delegated (mem/pids still enforced).
217
+ - **`secrets`** — `host:container` pairs, mounted read-only; the only way host files
218
+ enter the sandbox.
219
+
220
+ `ours-fleet doctor` reports bubblewrap availability, cgroup delegation, and each
221
+ role's effective isolation; `ours-fleet config` prints a per-role isolation summary.
222
+ Isolation composes with `model`, `permission_mode`, and `ROUTINES.md`. See
223
+ [SECURITY.md](SECURITY.md#agent-isolation-sandboxing) for the threat model and the
224
+ rootless prerequisites.
225
+
187
226
  ## Development
188
227
 
189
228
  ```sh
@@ -202,8 +241,33 @@ software built by a small independent team, running the broker and relay service
202
241
  at their own cost. If this is useful to you, please consider chipping in:
203
242
  **→ https://github.com/adapt-toolkit/ours-donate**
204
243
 
205
- ## License
244
+ ## Licence, status & warranty
245
+
246
+ > **Alpha software.** ours-fleet is part of **ours.network**, which is early,
247
+ > experimental, **alpha-stage** software. It is under active development, its
248
+ > behaviour and interfaces may change without notice, and it is **not
249
+ > production-ready**.
250
+
251
+ > **No warranty / not security-audited.** ours.network has **not** been
252
+ > independently security-audited. It is provided **"as is", without warranty of
253
+ > any kind**, and you use it **at your own risk**. See [`LICENSE`](LICENSE) and
254
+ > [`SECURITY.md`](SECURITY.md).
255
+
256
+ **ours.network** is owned and licensed by **Adapt Framework Solutions Ltd**. It
257
+ is released under the **Functional Source License, Version 1.1
258
+ ([FSL-1.1-Apache-2.0](LICENSE))** — **source-available, not open source** during
259
+ the FSL period. Each release **converts to Apache 2.0 two years after it is
260
+ published**.
261
+
262
+ The FSL permits any use **except a Competing Use** — broadly, offering a
263
+ commercial product or service that substitutes for, or provides substantially
264
+ the same functionality as, ours.network. Competing/commercial use requires a
265
+ separate **commercial licence** from Adapt Framework Solutions Ltd — see
266
+ [`COMMERCIAL-LICENCE.md`](COMMERCIAL-LICENCE.md) (contact:
267
+ **license@adaptframework.solutions**).
268
+
269
+ ours.network builds on Adapt Framework Solutions Ltd's own FSL-licensed core (the
270
+ `@adapt-toolkit` packages); **Adapt itself is not part of this release** and is
271
+ licensed separately.
206
272
 
207
- [FSL-1.1-Apache-2.0](LICENSE) Functional Source License, converting to
208
- Apache-2.0 two years after each release. Free for any use except offering a
209
- competing product or service. Copyright 2026 ours.network contributors.
273
+ Copyright 2026 Adapt Framework Solutions Ltd.
package/dist/cli.js CHANGED
@@ -56,6 +56,16 @@ cOpt(program.command('config').description('validate + print the merged plan (no
56
56
  console.log(` mission: ${r.mission.split('\n')[0]}`);
57
57
  if (r.oversee?.length)
58
58
  console.log(` oversees: ${r.oversee.map(o => `${o.role}@${o.interval}`).join(', ')}`);
59
+ if (r.isolation) {
60
+ const iso = r.isolation;
61
+ const caps = [
62
+ iso.resources?.mem && `mem=${iso.resources.mem}`,
63
+ iso.resources?.cpu && `cpu=${iso.resources.cpu}`,
64
+ iso.resources?.pids !== undefined && `pids=${iso.resources.pids}`,
65
+ ].filter(Boolean).join(',') || 'none';
66
+ console.log(` isolation: backend=${iso.backend ?? 'auto'} net=${iso.network ?? 'broker'} `
67
+ + `on_unavailable=${iso.on_unavailable ?? 'warn'} caps=${caps}`);
68
+ }
59
69
  }
60
70
  }
61
71
  catch (e) {
package/dist/doctor.js CHANGED
@@ -1,7 +1,24 @@
1
1
  import { userInfo } from 'node:os';
2
+ import { readFileSync } from 'node:fs';
2
3
  import { realExec } from './exec.js';
3
4
  import { loadConfig } from './config.js';
4
5
  import { getAdapter } from './harness/registry.js';
6
+ import { agentDir, home } from './paths.js';
7
+ import { resolveIsolation } from './isolation/policy.js';
8
+ import { makeBubblewrapBackend } from './isolation/bubblewrap.js';
9
+ /** Which cgroup-v2 controllers are delegated to this user manager (advisory). */
10
+ function cgroupDelegationDetail() {
11
+ try {
12
+ const uid = process.getuid?.() ?? 0;
13
+ const c = readFileSync(`/sys/fs/cgroup/user.slice/user-${uid}.slice/cgroup.controllers`, 'utf8').split(/\s+/);
14
+ const has = (n) => (c.includes(n) ? 'yes' : 'no');
15
+ return `memory=${has('memory')} pids=${has('pids')} cpu=${has('cpu')}` +
16
+ (c.includes('cpu') ? '' : ' — cpu caps degrade to a warning (one-time: Delegate=cpu)');
17
+ }
18
+ catch {
19
+ return 'unknown (not cgroup-v2 or no delegation info)';
20
+ }
21
+ }
5
22
  /** Host-level + per-harness prerequisite report with actionable messages. */
6
23
  export async function doctor(opts = {}, exec = realExec, platform = process.platform) {
7
24
  const checks = [];
@@ -37,9 +54,47 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
37
54
  : `not enabled — run: ours-fleet init (or: sudo loginctl enable-linger ${user})`,
38
55
  });
39
56
  }
57
+ // Isolation reporting (AC-9). Backend availability is advisory — isolation is
58
+ // opt-in per role (OQ-1), so a missing bwrap must not fail doctor for fleets that
59
+ // don't use it. Only a role that DECLARES isolation and cannot get it under
60
+ // `strict` is a hard failure.
61
+ const roles = loadConfigSafe(opts.configPath);
62
+ const bw = await makeBubblewrapBackend(exec).available();
63
+ checks.push({
64
+ name: 'isolation: bubblewrap', ok: true,
65
+ detail: bw.ok
66
+ ? `available — ${bw.detail}`
67
+ : `not available: ${bw.detail} (only needed for roles declaring isolation:)`,
68
+ });
69
+ if (platform === 'linux')
70
+ checks.push({ name: 'isolation: cgroup delegation', ok: true, detail: cgroupDelegationDetail() });
71
+ for (const r of roles.filter(r => r.isolation)) {
72
+ const stateDir = agentDir(r.name);
73
+ const policy = resolveIsolation(r.isolation, { stateDir, runCwd: r.cwd ?? stateDir, home: home() });
74
+ const caps = [
75
+ policy.resources.mem && `mem=${policy.resources.mem}`,
76
+ policy.resources.cpu && `cpu=${policy.resources.cpu}`,
77
+ policy.resources.pids !== undefined && `pids=${policy.resources.pids}`,
78
+ ].filter(Boolean).join(',') || 'none';
79
+ const wantsBwrap = policy.backend === 'auto' || policy.backend === 'bubblewrap';
80
+ let ok = true, detail;
81
+ if (policy.backend === 'none')
82
+ detail = 'backend=none (explicitly un-sandboxed)';
83
+ else if (wantsBwrap && bw.ok)
84
+ detail = `backend=bubblewrap net=${policy.network} caps=${caps}`;
85
+ else if (wantsBwrap && policy.onUnavailable === 'strict') {
86
+ ok = false;
87
+ detail = 'WILL REFUSE to launch (strict): bubblewrap unavailable';
88
+ }
89
+ else if (wantsBwrap)
90
+ detail = `degraded->un-isolated (warn): bubblewrap unavailable; caps=${caps} still apply`;
91
+ else
92
+ detail = `backend=${policy.backend} (not yet implemented)`;
93
+ checks.push({ name: `isolation: ${r.name}`, ok, detail });
94
+ }
40
95
  const harnesses = opts.harness
41
96
  ? [opts.harness]
42
- : [...new Set(loadConfigSafe(opts.configPath).map(r => r.harness))];
97
+ : [...new Set(roles.map(r => r.harness))];
43
98
  for (const h of harnesses) {
44
99
  try {
45
100
  const rep = await getAdapter(h).checkPrereqs();
package/dist/runner.js CHANGED
@@ -75,10 +75,15 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
75
75
  const ctx = { stateDir: dir, runCwd, home: home() };
76
76
  const policy = resolveIsolation(role.isolation, ctx);
77
77
  const sel = await selectIsolationBackend(policy, deps.exec); // throws on strict + unavailable
78
- if (sel.degraded)
78
+ const degradedMarker = join(dir, '.isolation-degraded');
79
+ if (sel.degraded) {
79
80
  deps.log(`[${name}] WARNING isolation requested but unavailable -> running UN-ISOLATED: ${sel.detail}`);
80
- else
81
+ writeFileSync(degradedMarker, `${new Date().toISOString()} ${sel.detail}\n`);
82
+ }
83
+ else {
81
84
  deps.log(`[${name}] isolation: ${sel.backend.id} (net=${policy.network}) ${sel.detail}`);
85
+ rmSync(degradedMarker, { force: true });
86
+ }
82
87
  paneArgv = sel.backend.wrap(launch.argv, policy, ctx);
83
88
  // Resource caps wrap the sandbox from OUTSIDE, at the pane's own cgroup scope
84
89
  // (§5.4). Applies even when the sandbox degraded to none.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, tmux consoles, systemd/launchd supervision, ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",