@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 +1 -1
- package/README.md +69 -5
- package/dist/cli.js +10 -0
- package/dist/doctor.js +56 -1
- package/dist/runner.js +7 -2
- package/package.json +1 -1
package/LICENSE
CHANGED
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
|
-
##
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|