@ours.network/fleet 0.5.0 → 0.5.2
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 +95 -10
- package/dist/cli.js +14 -1
- package/dist/doctor.js +66 -1
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +18 -0
- package/dist/runner.js +7 -2
- package/dist/supervisor/systemd.d.ts +7 -0
- package/dist/supervisor/systemd.js +13 -3
- 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?
|
|
@@ -26,8 +26,9 @@ reality match the file.
|
|
|
26
26
|
|
|
27
27
|
**Harness-agnostic by design.** The core never assumes a specific agent CLI; each
|
|
28
28
|
harness is a small adapter (how to launch, how to resume, how to wire config).
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
**Claude Code** is wired in, and the adapter interface is public — each
|
|
30
|
+
additional harness (Codex CLI, Gemini CLI, OpenCode, …) is a small adapter. A
|
|
31
|
+
single fleet can mix harnesses per role:
|
|
31
32
|
|
|
32
33
|
```yaml
|
|
33
34
|
roles:
|
|
@@ -178,12 +179,51 @@ roles:
|
|
|
178
179
|
# mem_palace: false # claude-code: disable memory plugin
|
|
179
180
|
# permission_mode: dontAsk # claude-code: launch permission mode —
|
|
180
181
|
# one of default | acceptEdits | plan | dontAsk | bypassPermissions
|
|
182
|
+
isolation: # OS-level sandbox (additive; omit = today's behavior)
|
|
183
|
+
backend: auto # auto | bubblewrap | podman | none (default auto)
|
|
184
|
+
on_unavailable: warn # warn (un-isolated + marker) | strict (refuse) (default warn)
|
|
185
|
+
network: broker # broker | deny | allow | allowlist (default broker)
|
|
186
|
+
fs: { read: [/opt/toolchains], write: [] } # extra binds (state dir + cwd always included)
|
|
187
|
+
resources: { mem: 2G, cpu: "1.5", pids: 512 }
|
|
188
|
+
secrets: ["/host/tok:/run/secrets/tok"] # host:container, mounted read-only
|
|
181
189
|
```
|
|
182
190
|
|
|
183
191
|
Merge order: `fleet.yaml` ← `fleet.d/*.yaml`; a duplicate role name is a hard
|
|
184
192
|
error naming both files. Identities and roles are decoupled — removing a role
|
|
185
193
|
never deletes an identity.
|
|
186
194
|
|
|
195
|
+
## Agent isolation
|
|
196
|
+
|
|
197
|
+
Each role can be sandboxed at the environment level via an `isolation:` block —
|
|
198
|
+
**fully additive: a role with no block behaves exactly as before.** The agent's
|
|
199
|
+
tmux-pane process is wrapped in [bubblewrap](https://github.com/containers/bubblewrap)
|
|
200
|
+
(rootless, no setuid), resource-limited by `systemd-run --user --scope`.
|
|
201
|
+
|
|
202
|
+
An empty `isolation: {}` gives a sensible default posture: filesystem-confined to
|
|
203
|
+
the state dir + `cwd`, the ours key store / other agents' state / `~/.ssh` / `~/.aws`
|
|
204
|
+
all invisible, ours messaging still works, no hard resource caps.
|
|
205
|
+
|
|
206
|
+
- **`backend`** — `auto` (bubblewrap if usable, else degrade per `on_unavailable`),
|
|
207
|
+
or force `bubblewrap` / `none`. (`podman` is planned.)
|
|
208
|
+
- **`on_unavailable`** — `warn` (default, fail-open: run un-isolated, log, and drop a
|
|
209
|
+
`.isolation-degraded` marker in the state dir) or `strict` (fail closed: refuse to launch).
|
|
210
|
+
- **`network`** — `broker` (default; ours messaging works), `deny` (no network),
|
|
211
|
+
`allow` (unrestricted), `allowlist` (planned). *Current status:* `deny` fully
|
|
212
|
+
unshares the network; `broker` keeps host networking so the loopback ours daemon
|
|
213
|
+
stays reachable — full broker egress-hardening is a follow-up.
|
|
214
|
+
- **`fs.read` / `fs.write`** — extra read-only / read-write binds on top of the durable set.
|
|
215
|
+
- **`resources`** — `mem` (→ `MemoryMax` + `MemorySwapMax=0`, a hard OOM bound),
|
|
216
|
+
`cpu` cores (→ `CPUQuota`), `pids` (→ `TasksMax`). CPU degrades to a warning if the
|
|
217
|
+
cpu cgroup controller isn't delegated (mem/pids still enforced).
|
|
218
|
+
- **`secrets`** — `host:container` pairs, mounted read-only; the only way host files
|
|
219
|
+
enter the sandbox.
|
|
220
|
+
|
|
221
|
+
`ours-fleet doctor` reports bubblewrap availability, cgroup delegation, and each
|
|
222
|
+
role's effective isolation; `ours-fleet config` prints a per-role isolation summary.
|
|
223
|
+
Isolation composes with `model`, `permission_mode`, and `ROUTINES.md`. See
|
|
224
|
+
[SECURITY.md](SECURITY.md#agent-isolation-sandboxing) for the threat model and the
|
|
225
|
+
rootless prerequisites.
|
|
226
|
+
|
|
187
227
|
## Development
|
|
188
228
|
|
|
189
229
|
```sh
|
|
@@ -195,15 +235,60 @@ Adding a harness = implementing `HarnessAdapter`
|
|
|
195
235
|
(`src/harness/types.ts`) and registering it — see `src/harness/claude-code.ts`
|
|
196
236
|
for the reference implementation.
|
|
197
237
|
|
|
238
|
+
## Learn more
|
|
239
|
+
|
|
240
|
+
- **The AI fleet use case:** a walkthrough of the coordinator-plus-specialists
|
|
241
|
+
pattern, end to end →
|
|
242
|
+
**[ours.network/use-cases/ai-fleet](https://ours.network/use-cases/ai-fleet)**.
|
|
243
|
+
- **How it works — the protocol, in depth:** the shared agent-to-agent core and
|
|
244
|
+
wire format is documented in
|
|
245
|
+
**[ours-mufl-core](https://github.com/adapt-toolkit/ours-mufl-core)**.
|
|
246
|
+
- **The whole project:** [ours.network](https://ours.network) ·
|
|
247
|
+
[umbrella repo](https://github.com/adapt-toolkit/ours-network)
|
|
248
|
+
|
|
198
249
|
## Support ours.network
|
|
199
250
|
|
|
200
|
-
ours
|
|
201
|
-
|
|
202
|
-
at
|
|
251
|
+
ours.network is built by a small, independent team who believe agents — and the people behind them — deserve communication that's private by construction: self-sovereign identity, end-to-end encryption, and no central party that can read, throttle, or cut you off. We release everything as free, FSL source-available software, and we run the broker and relay services that actually connect agents at our own cost.
|
|
252
|
+
|
|
253
|
+
We're at the alpha stage: we have a clear roadmap and, if this stage proves itself, proper funding will come later — but right now there is no funding and no monetization behind the project. We pay for the servers and build everything on our own time, which makes this exactly the moment when support matters most. Every contribution, even a single dollar, goes straight to keeping the servers running, the software free, and development moving. If ours.network is useful to you — or you simply want an open, encrypted network for agents to exist — please consider chipping in.
|
|
254
|
+
|
|
255
|
+
**Like it? Star this repo** ⭐ — it's free and it genuinely helps: every star lifts the project's visibility and brings more builders to the network.
|
|
256
|
+
|
|
203
257
|
**→ https://github.com/adapt-toolkit/ours-donate**
|
|
204
258
|
|
|
205
|
-
|
|
259
|
+
Thank you for helping keep it free, open, and alive.
|
|
260
|
+
|
|
261
|
+
## Licence, status & warranty
|
|
262
|
+
|
|
263
|
+
> **Alpha software.** ours-fleet is part of **ours.network**, which is early,
|
|
264
|
+
> experimental, **alpha-stage** software. It is under active development, its
|
|
265
|
+
> behaviour and interfaces may change without notice, and it is **not
|
|
266
|
+
> production-ready**.
|
|
267
|
+
|
|
268
|
+
> **No warranty / not security-audited.** ours.network has **not** been
|
|
269
|
+
> independently security-audited. It is provided **"as is", without warranty of
|
|
270
|
+
> any kind**, and you use it **at your own risk**. See [`LICENSE`](LICENSE) and
|
|
271
|
+
> [`SECURITY.md`](SECURITY.md).
|
|
272
|
+
|
|
273
|
+
**ours.network** is owned and licensed by **Adapt Framework Solutions Ltd**. It
|
|
274
|
+
is released under the **Functional Source License, Version 1.1
|
|
275
|
+
([FSL-1.1-Apache-2.0](LICENSE))** — **source-available, not open source** during
|
|
276
|
+
the FSL period. Each release **converts to Apache 2.0 two years after it is
|
|
277
|
+
published**.
|
|
278
|
+
|
|
279
|
+
The FSL permits any use **except a Competing Use** — broadly, offering a
|
|
280
|
+
commercial product or service that substitutes for, or provides substantially
|
|
281
|
+
the same functionality as, ours.network. Competing/commercial use requires a
|
|
282
|
+
separate **commercial licence** from Adapt Framework Solutions Ltd — see
|
|
283
|
+
[`COMMERCIAL-LICENCE.md`](COMMERCIAL-LICENCE.md) (contact:
|
|
284
|
+
**license@adaptframework.solutions**).
|
|
285
|
+
|
|
286
|
+
**Built on Adapt.** ours.network runs on ADAPT, a framework we've spent eight years building. ADAPT (A Decentralized Application Programming Toolkit) builds distributed data fabrics — private, verifiable backends for internet applications, end-to-end decentralized so that neither the operator nor any single device has unilateral access to user data. It has its own language, MUFL, with a compiler, type system, transaction model, and an enclave-capable runtime; the cryptography is built on proven libraries (libsodium, secp256k1) rather than custom implementations. Architecture, language and SDK reference: [docs.adaptframework.solutions](https://docs.adaptframework.solutions).
|
|
287
|
+
|
|
288
|
+
**Not a black box.** Much of the stack is already open and inspectable. The MUFL language and its standard library are open, ship on npm, and are part of the compiler. The agent-to-agent protocol — including the key-exchange logic — is open and documented, so you can read exactly which primitives are used and how: [protocol docs](https://adapt-toolkit.github.io/ours-mufl-core/). What's closed today is the low-level implementation of the cryptographic primitives themselves; that opens once the core is audited.
|
|
289
|
+
|
|
290
|
+
**Security by design, on three layers.** Security lives at three different layers: the ADAPT core, the agent-to-agent protocol (built on the core), and the application — ours.network's MCP server (built on the protocol). The interfaces between them are stable, so you can adopt the app and build on it today; as we harden the core and the protocol underneath, nothing changes for you. You inherit security by design instead of re-implementing it per app.
|
|
291
|
+
|
|
292
|
+
**Audit status.** The core has not yet had an independent security audit. We're raising funding to commission one from a recognized firm and prove these guarantees, and we'll open-source the full core once it passes. Until then it's source-available and documented, but not independently audited — run anything critical on it at your own risk.
|
|
206
293
|
|
|
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.
|
|
294
|
+
Copyright 2026 Adapt Framework Solutions Ltd.
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdirSync } from 'node:fs';
|
|
|
4
4
|
import { realpathSync } from 'node:fs';
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import { VERSION } from './version.js';
|
|
7
|
-
import { agentsRoot, tmpRoot, logsRoot } from './paths.js';
|
|
7
|
+
import { agentsRoot, tmpRoot, logsRoot, deriveXdgRuntimeDir } from './paths.js';
|
|
8
8
|
import { loadConfig } from './config.js';
|
|
9
9
|
import { Tmux } from './tmux.js';
|
|
10
10
|
import { pickBackend } from './supervisor/index.js';
|
|
@@ -13,6 +13,9 @@ import { runOnce, runTemp } from './runner.js';
|
|
|
13
13
|
import { spawnPermanent, spawnTemp } from './spawn.js';
|
|
14
14
|
import { doctor } from './doctor.js';
|
|
15
15
|
import './harness/claude-code.js'; // registers the claude-code adapter
|
|
16
|
+
// sudo/su shells lack XDG_RUNTIME_DIR, breaking every systemctl/journalctl
|
|
17
|
+
// --user child (supervisor commands, logs, doctor). Derive it before dispatch. (#9)
|
|
18
|
+
deriveXdgRuntimeDir();
|
|
16
19
|
const binPath = (() => { try {
|
|
17
20
|
return realpathSync(process.argv[1]);
|
|
18
21
|
}
|
|
@@ -56,6 +59,16 @@ cOpt(program.command('config').description('validate + print the merged plan (no
|
|
|
56
59
|
console.log(` mission: ${r.mission.split('\n')[0]}`);
|
|
57
60
|
if (r.oversee?.length)
|
|
58
61
|
console.log(` oversees: ${r.oversee.map(o => `${o.role}@${o.interval}`).join(', ')}`);
|
|
62
|
+
if (r.isolation) {
|
|
63
|
+
const iso = r.isolation;
|
|
64
|
+
const caps = [
|
|
65
|
+
iso.resources?.mem && `mem=${iso.resources.mem}`,
|
|
66
|
+
iso.resources?.cpu && `cpu=${iso.resources.cpu}`,
|
|
67
|
+
iso.resources?.pids !== undefined && `pids=${iso.resources.pids}`,
|
|
68
|
+
].filter(Boolean).join(',') || 'none';
|
|
69
|
+
console.log(` isolation: backend=${iso.backend ?? 'auto'} net=${iso.network ?? 'broker'} `
|
|
70
|
+
+ `on_unavailable=${iso.on_unavailable ?? 'warn'} caps=${caps}`);
|
|
71
|
+
}
|
|
59
72
|
}
|
|
60
73
|
}
|
|
61
74
|
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, deriveXdgRuntimeDir } 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 = [];
|
|
@@ -36,10 +53,58 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
36
53
|
detail: ok ? 'enabled (roles survive logout/reboot)'
|
|
37
54
|
: `not enabled — run: ours-fleet init (or: sudo loginctl enable-linger ${user})`,
|
|
38
55
|
});
|
|
56
|
+
// systemctl --user needs $XDG_RUNTIME_DIR/bus. The cli entry point derives
|
|
57
|
+
// it from /run/user/<uid> when possible (#9), so a failure here means the
|
|
58
|
+
// user manager itself is unreachable — sudo/su shell with linger off.
|
|
59
|
+
const xdg = deriveXdgRuntimeDir();
|
|
60
|
+
checks.push({
|
|
61
|
+
name: 'user bus', ok: !!xdg,
|
|
62
|
+
detail: xdg
|
|
63
|
+
? `XDG_RUNTIME_DIR=${xdg}`
|
|
64
|
+
: `no XDG_RUNTIME_DIR and /run/user/<uid> missing — systemctl --user cannot reach the user manager; enable linger: sudo loginctl enable-linger ${user}`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// Isolation reporting (AC-9). Backend availability is advisory — isolation is
|
|
68
|
+
// opt-in per role (OQ-1), so a missing bwrap must not fail doctor for fleets that
|
|
69
|
+
// don't use it. Only a role that DECLARES isolation and cannot get it under
|
|
70
|
+
// `strict` is a hard failure.
|
|
71
|
+
const roles = loadConfigSafe(opts.configPath);
|
|
72
|
+
const bw = await makeBubblewrapBackend(exec).available();
|
|
73
|
+
checks.push({
|
|
74
|
+
name: 'isolation: bubblewrap', ok: true,
|
|
75
|
+
detail: bw.ok
|
|
76
|
+
? `available — ${bw.detail}`
|
|
77
|
+
: `not available: ${bw.detail} (only needed for roles declaring isolation:)`,
|
|
78
|
+
});
|
|
79
|
+
if (platform === 'linux')
|
|
80
|
+
checks.push({ name: 'isolation: cgroup delegation', ok: true, detail: cgroupDelegationDetail() });
|
|
81
|
+
for (const r of roles.filter(r => r.isolation)) {
|
|
82
|
+
const stateDir = agentDir(r.name);
|
|
83
|
+
const policy = resolveIsolation(r.isolation, { stateDir, runCwd: r.cwd ?? stateDir, home: home() });
|
|
84
|
+
const caps = [
|
|
85
|
+
policy.resources.mem && `mem=${policy.resources.mem}`,
|
|
86
|
+
policy.resources.cpu && `cpu=${policy.resources.cpu}`,
|
|
87
|
+
policy.resources.pids !== undefined && `pids=${policy.resources.pids}`,
|
|
88
|
+
].filter(Boolean).join(',') || 'none';
|
|
89
|
+
const wantsBwrap = policy.backend === 'auto' || policy.backend === 'bubblewrap';
|
|
90
|
+
let ok = true, detail;
|
|
91
|
+
if (policy.backend === 'none')
|
|
92
|
+
detail = 'backend=none (explicitly un-sandboxed)';
|
|
93
|
+
else if (wantsBwrap && bw.ok)
|
|
94
|
+
detail = `backend=bubblewrap net=${policy.network} caps=${caps}`;
|
|
95
|
+
else if (wantsBwrap && policy.onUnavailable === 'strict') {
|
|
96
|
+
ok = false;
|
|
97
|
+
detail = 'WILL REFUSE to launch (strict): bubblewrap unavailable';
|
|
98
|
+
}
|
|
99
|
+
else if (wantsBwrap)
|
|
100
|
+
detail = `degraded->un-isolated (warn): bubblewrap unavailable; caps=${caps} still apply`;
|
|
101
|
+
else
|
|
102
|
+
detail = `backend=${policy.backend} (not yet implemented)`;
|
|
103
|
+
checks.push({ name: `isolation: ${r.name}`, ok, detail });
|
|
39
104
|
}
|
|
40
105
|
const harnesses = opts.harness
|
|
41
106
|
? [opts.harness]
|
|
42
|
-
: [...new Set(
|
|
107
|
+
: [...new Set(roles.map(r => r.harness))];
|
|
43
108
|
for (const h of harnesses) {
|
|
44
109
|
try {
|
|
45
110
|
const rep = await getAdapter(h).checkPrereqs();
|
package/dist/paths.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
/** Home root for config + state. OURS_FLEET_HOME overrides (tests, exotic setups). */
|
|
2
2
|
export declare const home: () => string;
|
|
3
|
+
/**
|
|
4
|
+
* systemctl/journalctl --user locate the user bus via $XDG_RUNTIME_DIR/bus.
|
|
5
|
+
* sudo/su shells run inside the CALLING user's logind session, so the target
|
|
6
|
+
* user's shell gets no XDG_RUNTIME_DIR even when linger keeps the user manager
|
|
7
|
+
* (and the bus socket) alive at /run/user/<uid>. Derive the standard path once
|
|
8
|
+
* at CLI startup: never override an existing value, and only fire when the dir
|
|
9
|
+
* actually exists — when it doesn't, the real problem is missing linger and the
|
|
10
|
+
* systemctl error (plus its hint) is the right signal. (#9)
|
|
11
|
+
*/
|
|
12
|
+
export declare function deriveXdgRuntimeDir(env?: NodeJS.ProcessEnv, uid?: number | undefined, exists?: (p: string) => boolean): string | undefined;
|
|
3
13
|
export declare const stateRoot: () => string;
|
|
4
14
|
export declare const agentsRoot: () => string;
|
|
5
15
|
export declare const tmpRoot: () => string;
|
package/dist/paths.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import { homedir } from 'node:os';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
/** Home root for config + state. OURS_FLEET_HOME overrides (tests, exotic setups). */
|
|
4
5
|
export const home = () => process.env.OURS_FLEET_HOME ?? homedir();
|
|
6
|
+
/**
|
|
7
|
+
* systemctl/journalctl --user locate the user bus via $XDG_RUNTIME_DIR/bus.
|
|
8
|
+
* sudo/su shells run inside the CALLING user's logind session, so the target
|
|
9
|
+
* user's shell gets no XDG_RUNTIME_DIR even when linger keeps the user manager
|
|
10
|
+
* (and the bus socket) alive at /run/user/<uid>. Derive the standard path once
|
|
11
|
+
* at CLI startup: never override an existing value, and only fire when the dir
|
|
12
|
+
* actually exists — when it doesn't, the real problem is missing linger and the
|
|
13
|
+
* systemctl error (plus its hint) is the right signal. (#9)
|
|
14
|
+
*/
|
|
15
|
+
export function deriveXdgRuntimeDir(env = process.env, uid = process.getuid?.(), exists = existsSync) {
|
|
16
|
+
if (!env.XDG_RUNTIME_DIR && uid !== undefined) {
|
|
17
|
+
const runDir = `/run/user/${uid}`;
|
|
18
|
+
if (exists(runDir))
|
|
19
|
+
env.XDG_RUNTIME_DIR = runDir;
|
|
20
|
+
}
|
|
21
|
+
return env.XDG_RUNTIME_DIR;
|
|
22
|
+
}
|
|
5
23
|
export const stateRoot = () => join(home(), '.ours-fleet');
|
|
6
24
|
export const agentsRoot = () => join(stateRoot(), 'agents');
|
|
7
25
|
export const tmpRoot = () => join(stateRoot(), 'tmp');
|
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.
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { type Exec } from '../exec.js';
|
|
2
2
|
import type { SupervisorBackend } from './types.js';
|
|
3
3
|
export declare const UNIT_TEMPLATE = "ours-fleet-agent@.service";
|
|
4
|
+
/**
|
|
5
|
+
* Actionable hint when systemctl cannot reach the user bus. After the cli.ts
|
|
6
|
+
* XDG_RUNTIME_DIR fallback this branch stays reachable only when
|
|
7
|
+
* /run/user/<uid> itself is missing — i.e. linger is off and no session is
|
|
8
|
+
* active — so pointing at linger is the correct first hint. (#9)
|
|
9
|
+
*/
|
|
10
|
+
export declare const busHint: (stderr: string) => string;
|
|
4
11
|
export declare const unitFor: (name: string) => string;
|
|
5
12
|
export declare function makeSystemdBackend(exec?: Exec): SupervisorBackend;
|
|
@@ -4,6 +4,16 @@ import { userInfo } from 'node:os';
|
|
|
4
4
|
import { home } from '../paths.js';
|
|
5
5
|
import { realExec } from '../exec.js';
|
|
6
6
|
export const UNIT_TEMPLATE = 'ours-fleet-agent@.service';
|
|
7
|
+
/**
|
|
8
|
+
* Actionable hint when systemctl cannot reach the user bus. After the cli.ts
|
|
9
|
+
* XDG_RUNTIME_DIR fallback this branch stays reachable only when
|
|
10
|
+
* /run/user/<uid> itself is missing — i.e. linger is off and no session is
|
|
11
|
+
* active — so pointing at linger is the correct first hint. (#9)
|
|
12
|
+
*/
|
|
13
|
+
export const busHint = (stderr) => /user scope bus|XDG_RUNTIME_DIR/.test(stderr)
|
|
14
|
+
? `\nhint: no user runtime dir — enable linger: sudo loginctl enable-linger ${userInfo().username}` +
|
|
15
|
+
`\n (if linger is already on: export XDG_RUNTIME_DIR=/run/user/$(id -u))`
|
|
16
|
+
: '';
|
|
7
17
|
export const unitFor = (name) => `ours-fleet-agent@${name}.service`;
|
|
8
18
|
export function makeSystemdBackend(exec = realExec) {
|
|
9
19
|
const ctl = (...args) => exec('systemctl', ['--user', ...args]);
|
|
@@ -38,18 +48,18 @@ WantedBy=default.target
|
|
|
38
48
|
async install(name) {
|
|
39
49
|
const r = await ctl('enable', '--now', unitFor(name));
|
|
40
50
|
if (r.code !== 0)
|
|
41
|
-
throw new Error(`systemctl enable --now ${unitFor(name)} failed: ${r.stderr.trim()}`);
|
|
51
|
+
throw new Error(`systemctl enable --now ${unitFor(name)} failed: ${r.stderr.trim()}${busHint(r.stderr)}`);
|
|
42
52
|
},
|
|
43
53
|
async start(name) { await ctl('start', unitFor(name)); },
|
|
44
54
|
async stop(name) {
|
|
45
55
|
const r = await ctl('stop', unitFor(name));
|
|
46
56
|
if (r.code !== 0)
|
|
47
|
-
throw new Error(`systemctl stop ${unitFor(name)} failed: ${r.stderr.trim()}`);
|
|
57
|
+
throw new Error(`systemctl stop ${unitFor(name)} failed: ${r.stderr.trim()}${busHint(r.stderr)}`);
|
|
48
58
|
},
|
|
49
59
|
async restart(name) {
|
|
50
60
|
const r = await ctl('restart', unitFor(name));
|
|
51
61
|
if (r.code !== 0)
|
|
52
|
-
throw new Error(`systemctl restart ${unitFor(name)} failed: ${r.stderr.trim()}`);
|
|
62
|
+
throw new Error(`systemctl restart ${unitFor(name)} failed: ${r.stderr.trim()}${busHint(r.stderr)}`);
|
|
53
63
|
},
|
|
54
64
|
async status(name) {
|
|
55
65
|
const r = await ctl('status', unitFor(name), '--no-pager');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
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",
|