@parcha/pi-durable-disk 0.1.0-beta.21 → 0.1.0-beta.26
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/CHANGELOG.md +33 -1
- package/README.md +12 -8
- package/dist/claim.d.ts +1 -1
- package/dist/cli.d.ts +11 -2
- package/dist/cli.js +63 -18
- package/dist/env.js +3 -0
- package/dist/hosts/attempt-env.d.ts +9 -0
- package/dist/hosts/attempt-env.js +12 -1
- package/dist/hosts/daytona.d.ts +1 -1
- package/dist/hosts/daytona.js +2 -0
- package/dist/hosts/docker.d.ts +1 -1
- package/dist/hosts/docker.js +1 -0
- package/dist/hosts/local-host.d.ts +1 -1
- package/dist/hosts/local-host.js +119 -26
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/place.d.ts +64 -1
- package/dist/place.js +133 -4
- package/dist/run-files.d.ts +72 -0
- package/dist/run-files.js +259 -0
- package/dist/supervise.d.ts +12 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.0-beta.26 (2026-10-24)
|
|
6
|
+
|
|
7
|
+
- Pin the Docker image, planned-move example and live-test sandbox setup to Archil client 0.10.0, with SHA-256 checks for each downloaded package. The client verifies server certificates against the operating system trust store and rejects certificates from the compromised eMudhra intermediate authorities. Linux hosts need client 0.10.0 or later and the operating system CA bundle installed (`ca-certificates` on Debian/Ubuntu); rebuild Docker images and sandbox snapshots to use the new client.
|
|
8
|
+
- Archil's latest SDK: the package depends on the `disk` SDK 1.9.0 (was 1.7.0). The SDK calls the claim, lease and supervisor make are the same in 1.9.0, which adds disk branches and sandbox mounts. The client verbs the package runs take the same arguments in client 0.10.0, whose `mount` adds `--kernel-read-ahead-kb`. A host that installs `disk` itself installs 1.9.0, so npm keeps one copy.
|
|
9
|
+
- `archilPlace({ unheld })` takes a hook that runs in each start's window while nobody holds the run: after ensureRunning's revoke and start mark and after the run's directories are made, before the driver starts anything, once per start. It gets the start's `attempt` and, when the place hands labels over, exactly the `HOST_BOX` that start carries (`start.box`). It also gets the run's own files: `read`, `list`, and `put(path, text)`, which creates a file the run user owns (0644, parents 0755) and refuses a path already there with `EXISTS`. What it returns is added to that start's environment. It needs `ensure.create` and a control that lists. New types `UnheldHook`, `UnheldRun` and `UnheldStart`, and new `unheldTimeoutMs` (default 30 s).
|
|
10
|
+
- A put the hook does not await still settles inside the window: the driver starts only once every put has settled, within the hook's bound.
|
|
11
|
+
- `put` is a check, then a write, not a conditional create, so it is not exclusive between two supervisors in one window: put names unique to the start, and bind a shared name in the process that holds the run. It is for non-secret run files; a secret never goes through it.
|
|
12
|
+
- A start fails, and the driver starts no box, when a put was refused because the run is held (`WINDOW_CLOSED`, even if the hook caught it), when the hook throws a control failure (`CONTROL_API_FAILED`), when it throws anything else or overruns its bound (`UNHELD_FAILED`), or when its environment has a name the start already sets, a name that is not a variable name, a key-shaped name (a `KEY`, `TOKEN`, `SECRET`, `PASSWORD`, `PASSWD`, `CREDENTIAL` or `CREDENTIALS` segment), or a value that is not a string without NUL (`INVALID_ARGUMENT`, naming the variable, never its value). The caller sees `HOST_START_FAILED` with that `PlaceError` as its cause. `RunFileErrorCode` gains `EXISTS` and `WINDOW_CLOSED`; `PlaceErrorCode` gains `WINDOW_CLOSED`, `CONTROL_API_FAILED` and `UNHELD_FAILED`.
|
|
13
|
+
- Fix to merged behavior: a box the place already started and its driver adopts again (`adopted: true`, the same attempt started twice) keeps its key in `status().boxes`. New optional `HostDriver.instanceOf(handle)` names the instance a handle is for, the same for its fresh and its adopted handle; `dockerHost` (daemon and container name), `localHost` (host, boot, unit or tag) and `daytonaHost` (sandbox id) implement it, and without it no two handles are one box. It used to get a second entry: a key no runner claimed under `HOST_BOX`, the box counted twice in the receipts, and stopped twice at release.
|
|
14
|
+
- `readRunFile(control, ref, path)` and `listRunFiles(control, ref, dir)` read a run's own files over the disk's control (S3 GET, HEAD and LIST): they never mount, write, or take or wait on the claim, so they read a run another host holds. `readRunFile` returns the bytes, or `null` for a file not written yet; `listRunFiles` returns the names directly under a directory, sorted, names only. Refusals are `RunFileError`: `INVALID_PATH` (before any request: absolute, `.`/`..`, unsafe characters, `.archil/`), `RUN_MISSING`, `NOT_A_FILE` (a directory, or a symlink, decided on Archil's 501 for it, never from a listing, where a symlink looks like an empty file), and `CONTROL_API_FAILED`. Both take a `signal`.
|
|
15
|
+
- A place offers the same reads of its own run: `Place.readRunFile(path)` and `Place.listRunFiles(dir)`, optional members that `archilPlace` implements over its control and `localPlace` over its run directory on this machine, where a symlink anywhere on the way is `NOT_A_FILE`, never followed. A missing file is `null`; a directory not there yet lists `[]`; every other failure throws.
|
|
16
|
+
- `INVALID_PATH` names what is wrong with a path (empty, absolute, a `.` or `..` segment, an unsafe character at an index, under `.archil/`), never the path itself.
|
|
17
|
+
- `archilPlace` tells each host box's runner which label to claim: when its driver makes boxes and names their provider (new optional `HostDriver.provider`; `daytonaHost` names `daytona`) and the place can read the label back (its control lists), each start puts `HOST_BOX` (`{ provider, key, run }`: the box's index in `status().boxes`, the order's `RUN_LABEL`) in that order's environment. With it, `status()` fails (`STATUS_FAILED`) on a box its driver gives no receipt for, instead of shifting the keys of the boxes after it.
|
|
18
|
+
|
|
19
|
+
## 0.1.0-beta.23 (2026-10-22)
|
|
20
|
+
|
|
21
|
+
- The CLI's duration flags take a unit: `ms`, `s`, `m` or `h` (`500ms`, `30s`, `1.5m`, `1h`). A bare number used to mean milliseconds, so `--every 90` ran every 90 ms. It is now refused with exit 2 and a hint: `not a duration: "90": give a unit, e.g. 90s or 90ms`. Every duration flag a command is given is checked before the command acts. **Who breaks:** a script or service unit that passes a bare number to `--park-threshold`, `--park-idle`, `--drain-timeout`, `--stop-timeout`, `--control-timeout`, `--lease-expiry`, `--stonith-timeout`, `--start-grace`, `--start-backoff-max`, `--token-grace` or `--every`. **What to do:** add the unit you meant (`90ms` keeps today's behaviour).
|
|
22
|
+
- Flags named -ms keep bare milliseconds (`--heartbeat-ms`, `--lease-expiry-ms`, `--lease-margin-ms`): their name carries the unit, and units are still accepted.
|
|
23
|
+
- A bare `0` still turns off `--park-threshold` (no parking), `--start-grace` (no grace, so no backoff) and `--start-backoff-max` (no backoff). Elsewhere `0` takes a unit (`0s`) and behaves as before.
|
|
24
|
+
- **SECURITY: `localHost` stops and kills only a unit it could have started, and says so when it refuses one.** A handle comes back from `run.json`, which the run's user can write, and `stop` and `kill` act on it as root (`systemctl stop` and `kill`, `archil unmount`, `fusermount`, `umount`, `rmdir`). Before, only the shape of the unit's name was checked, so a handle that named another service on the host had it stopped. Now `stop` and `kill` act only on a unit with the driver's `unitPrefix` whose mountpoint is one run's under the driver's mount root, as `dockerHost` and `daytonaHost` act only on their fleet's. For any other handle of this host and boot, and for a child-mode handle whose pid is 1 or below or that has no start ticks, they throw `INVALID_ARGUMENT`, naming the handle and saying that nothing was stopped or killed. `supervise` logs that STONITH as `failed` with the reason, where it used to log `stopped`, then revokes and starts as before. A handle of another host or boot, and a child that is already gone, are still no-ops; `status` is unchanged.
|
|
25
|
+
|
|
26
|
+
- Who notices: a supervisor that changed `--unit-prefix` or `--mount-root` between runs no longer stops the units started under the old values. Their STONITH logs the refusal, and the revoke fences them as before.
|
|
27
|
+
- **SECURITY: `localHost` and `archilEnv` refuse to run the agent as root.** The docs say instances and the agent's tools never run as root, and `--run-as`, `daytonaHost` and the in-box launcher already refused uid 0. `localHost` only required a `user` when the supervisor runs as root, so `user: "root"`, `"0"` or `0` started the unit with `User=0`, and child mode in a root process ran the instance as root; `archilEnv` accepted `runAs: { uid: 0 }`. Now `localHost.start` throws `INVALID_ARGUMENT` for a user that is uid 0 (a name is looked up in `/etc/passwd`) or empty (systemd runs a unit with an empty `User=` as root), and for child mode in a root process, and `archilEnv` throws `INVALID_RUN_AS` for a `runAs` uid of 0, each before anything runs. A driver is constructed as before, so the `status` command's read-only driver is unaffected.
|
|
28
|
+
- **SECURITY: `localHost` in systemd mode no longer puts the instance environment on the `systemd-run` command line, which sudo logged to the journal and `systemctl show` exposed; it passes it through a root-only file.**
|
|
29
|
+
- Before, each variable of the driver's `env` and of a start's own environment (`StartAttempt.env`) went to `systemd-run` as `--setenv=NAME=value`, under `sudo -n`. `sudo` logs the whole command line to the journal, and `--setenv` is stored in the unit's `Environment=` property, which any local user reads with `systemctl show`. A secret in either place was in both. The mount token and the API key were never affected: the token travels on stdin and the API key never reaches the driver.
|
|
30
|
+
- Now the environment is written to a root-only (mode 0600) file beside the mount token's, through stdin, and the unit loads it with `-p EnvironmentFile=<path>`. No value is on any argument. The file lives as long as the unit may restart (a restart in place loads it again). It is removed when the unit is stopped, when a start fails, and by the unit itself (`ExecStopPost=`) when it ends for good: a park or a finish, or a terminal exit. A unit that systemd gave up restarting keeps it until `stop`. A value with a newline, a carriage return or a NUL, or a name that is not a variable name, is refused with `INVALID_ARGUMENT` before anything runs, rather than written mangled.
|
|
31
|
+
- A deployment that put a secret in `localHost`'s `env` or in `StartAttempt.env` under systemd mode should treat it as exposed to anyone who could read its journal or run `systemctl show` on the unit, and rotate it. Child mode is unaffected: its environment goes in `spawn`'s `env`.
|
|
32
|
+
- Every install line in the README and the examples pins `@earendil-works/pi-durable`, `@earendil-works/pi-ai` and `@earendil-works/chord` at 1.3.0. pi 1.4.0 is now npm's `latest`, and an unpinned line ends in ERESOLVE against the peer ranges (`>=1.0.4 <1.4.0`). pi 1.4 is not supported yet; a later release widens the range.
|
|
33
|
+
- The README opens with the agent as the workspace, and the package description is its short form: its conversation and its files live together in
|
|
34
|
+
one directory on a disk, which any Linux host or container can mount, release and mount somewhere else, and the agent carries on
|
|
35
|
+
where the last host released it. The second paragraph says what opening and releasing a run do on an Archil disk (mount and
|
|
36
|
+
claim, then seal, give up the claim and unmount) and on a plain directory (no mount).
|
|
37
|
+
|
|
5
38
|
## 0.1.0-beta.21 (2026-10-21)
|
|
6
39
|
|
|
7
40
|
- `supervise --no-restart` is in the usage text and the reference: it turns off the local driver's restart on failure (`localHost({ restart: false })`), so an instance that exits stays down until the supervisor's next pass.
|
|
@@ -513,7 +546,6 @@ published.
|
|
|
513
546
|
- Unchanged, because they name the Archil disk: `bin/archil-scoped` (and the `./bin/archil-scoped` export), `ARCHIL_API_KEY`, `archilEnv`,
|
|
514
547
|
`ArchilCodingTools`, the claim's internals and the `archil` client it runs.
|
|
515
548
|
|
|
516
|
-
|
|
517
549
|
**A holder the control API lists without a path**
|
|
518
550
|
- Archil's control API resolves a delegation's path best-effort and can list one with no path, live or orphaned, even on a
|
|
519
551
|
run id used once. `findDelegations` matched by path only, so such a holder read as none everywhere it is used
|
package/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# @parcha/pi-durable-disk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The agent is the workspace: its conversation and its files live together in one directory on a disk. Mount the disk on any Linux host or container, release it, and mount it somewhere else: the agent carries on where the last host released it.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
(
|
|
5
|
+
On an [Archil](https://archil.com) disk, opening a run mounts its directory (`runs/<id>` of the disk) and takes an exclusive claim on it, and `release()` seals the
|
|
6
|
+
run, gives up the claim and unmounts. On a plain directory of this machine (`localRun`) there is no mount: one machine, no disk,
|
|
7
|
+
no key. It runs [pi-durable](https://www.npmjs.com/package/@earendil-works/pi-durable) agents, on Linux (macOS is for development;
|
|
8
|
+
Windows is not supported). This is a beta (`0.1.0-beta.N`): a change that breaks a caller is listed in [CHANGELOG.md](CHANGELOG.md)
|
|
9
|
+
when it ships.
|
|
8
10
|
|
|
9
11
|
## What it is
|
|
10
12
|
|
|
@@ -42,9 +44,11 @@ Archil is not supported: the package needs the disk to give an exclusive claim a
|
|
|
42
44
|
On Linux with Node 22.19 or later (or on macOS [for development](docs/reference.md#on-macos)). In an empty directory:
|
|
43
45
|
|
|
44
46
|
```sh
|
|
45
|
-
npm install @parcha/pi-durable-disk@beta @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
|
|
47
|
+
npm install @parcha/pi-durable-disk@beta @earendil-works/pi-durable@1.3.0 @earendil-works/pi-ai@1.3.0 @earendil-works/chord@1.3.0
|
|
46
48
|
```
|
|
47
49
|
|
|
50
|
+
pi 1.4 is not supported yet; a later release widens the range.
|
|
51
|
+
|
|
48
52
|
Save this as `pick-up.mts` (Node 22.19 or later runs the types away; `.mts` makes it an ES module whatever your
|
|
49
53
|
`package.json` says). It is [examples/pick-up-a-run/main.ts](examples/pick-up-a-run/main.ts). It starts three processes, one after another, on
|
|
50
54
|
one directory. Each opens the run, answers one question of the same chat, and releases it. pi-ai's faux model answers, so it
|
|
@@ -216,11 +220,11 @@ what it imports.
|
|
|
216
220
|
## Install
|
|
217
221
|
|
|
218
222
|
```sh
|
|
219
|
-
npm install @parcha/pi-durable-disk@beta @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
|
|
223
|
+
npm install @parcha/pi-durable-disk@beta @earendil-works/pi-durable@1.3.0 @earendil-works/pi-ai@1.3.0 @earendil-works/chord@1.3.0
|
|
220
224
|
```
|
|
221
225
|
|
|
222
|
-
`@earendil-works/pi-durable` and `@earendil-works/chord` are peer dependencies, `>=1.0.4 <1.4.0
|
|
223
|
-
Linux, or macOS for development.
|
|
226
|
+
`@earendil-works/pi-durable` and `@earendil-works/chord` are peer dependencies, `>=1.0.4 <1.4.0`: pi 1.4 is not supported yet; a
|
|
227
|
+
later release widens the range. Node 22.19 or later; Linux, or macOS for development.
|
|
224
228
|
|
|
225
229
|
From a checkout (this package is `packages/pi-durable-disk` of the repository; the root build compiles `dist/`):
|
|
226
230
|
|
package/dist/claim.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export type RunRef = {
|
|
|
4
4
|
region: string;
|
|
5
5
|
id: string;
|
|
6
6
|
};
|
|
7
|
-
/** The control-plane calls the claim uses; an archil `Disk` (SDK `disk` 1.
|
|
7
|
+
/** The control-plane calls the claim uses; an archil `Disk` (SDK `disk` 1.9.0) satisfies it. Supervisor side only. */
|
|
8
8
|
export interface ControlApi {
|
|
9
9
|
putObject(key: string, body: string, options: {
|
|
10
10
|
uid: number;
|
package/dist/cli.d.ts
CHANGED
|
@@ -20,8 +20,17 @@ export declare function watchdogOwnsCgroup(holder: Record<string, unknown>, opti
|
|
|
20
20
|
owns: boolean;
|
|
21
21
|
warning?: string;
|
|
22
22
|
};
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* A duration: a number and its unit, `ms`, `s`, `m` or `h` ("500ms", "30s", "1.5m", "1h"). A bare number is refused with
|
|
25
|
+
* a hint, so "90" is never silently 90 ms. A flag whose name carries its unit (`bareMs`, the `-ms` flags) takes a bare
|
|
26
|
+
* number as milliseconds; a flag whose 0 turns its feature off (`zeroOff`) also takes a bare `0`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseDuration(text: string, rule?: DurationRule): number;
|
|
29
|
+
/** How a duration flag reads a bare number: as milliseconds (`bareMs`), or a bare `0` as off (`zeroOff`); otherwise never. */
|
|
30
|
+
export type DurationRule = {
|
|
31
|
+
readonly bareMs?: boolean;
|
|
32
|
+
readonly zeroOff?: boolean;
|
|
33
|
+
};
|
|
25
34
|
/** `--run-as NAME|UID[:GID]`: the user the agent's commands run as, with its group and home from /etc/passwd when listed. */
|
|
26
35
|
export declare function runUser(spec: string, passwd?: string): {
|
|
27
36
|
uid: number;
|
package/dist/cli.js
CHANGED
|
@@ -50,7 +50,9 @@ const USAGE = `usage:
|
|
|
50
50
|
pi-durable-disk status --disk D --region R --id ID [--host-name NAME] [--api-key-env NAME] [--docker BIN]
|
|
51
51
|
pi-durable-disk release --id ID [--mount-root DIR]
|
|
52
52
|
pi-durable-disk fork --disk D --region R --id ID --new-id NEW [--mount-root DIR] [--archil BIN] [--token-prefix P]
|
|
53
|
-
[--api-key-env NAME]
|
|
53
|
+
[--api-key-env NAME]
|
|
54
|
+
durations take a unit (500ms, 30s, 1.5m, 1h); flags named -ms take bare milliseconds; a bare 0 turns off
|
|
55
|
+
--park-threshold, --start-grace and --start-backoff-max`;
|
|
54
56
|
/**
|
|
55
57
|
* Whether the lease watchdog may kill every other process in the instance's cgroup: only inside its own systemd unit,
|
|
56
58
|
* whose cgroup holds nothing but the instance and its commands. Elsewhere the cgroup may be shared (a login session, an
|
|
@@ -89,20 +91,63 @@ export function watchdogOwnsCgroup(holder, options = {}) {
|
|
|
89
91
|
return refuse(`the parent shares cgroup ${own}, so this process only inherited the handle`);
|
|
90
92
|
return { owns: true };
|
|
91
93
|
}
|
|
92
|
-
/**
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
/**
|
|
95
|
+
* A duration: a number and its unit, `ms`, `s`, `m` or `h` ("500ms", "30s", "1.5m", "1h"). A bare number is refused with
|
|
96
|
+
* a hint, so "90" is never silently 90 ms. A flag whose name carries its unit (`bareMs`, the `-ms` flags) takes a bare
|
|
97
|
+
* number as milliseconds; a flag whose 0 turns its feature off (`zeroOff`) also takes a bare `0`.
|
|
98
|
+
*/
|
|
99
|
+
export function parseDuration(text, rule = {}) {
|
|
100
|
+
const t = text.trim();
|
|
101
|
+
const m = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(t);
|
|
95
102
|
if (!m)
|
|
96
|
-
throw new UsageError(`not a duration: ${text}`);
|
|
97
|
-
|
|
103
|
+
throw new UsageError(`not a duration: "${text}": a number and its unit (ms, s, m or h), e.g. 90s`);
|
|
104
|
+
if (m[2] === undefined) {
|
|
105
|
+
if (rule.bareMs)
|
|
106
|
+
return Math.round(Number(m[1]));
|
|
107
|
+
if (rule.zeroOff && Number(m[1]) === 0)
|
|
108
|
+
return 0;
|
|
109
|
+
throw new UsageError(`not a duration: "${text}": give a unit, e.g. ${t}s or ${t}ms`);
|
|
110
|
+
}
|
|
111
|
+
return Math.round(Number(m[1]) * { ms: 1, s: 1000, m: 60_000, h: 3_600_000 }[m[2]]);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Every duration flag of every command, with its rule. The `-ms` flags carry their unit in their name. A bare `0` turns
|
|
115
|
+
* off parking (`--park-threshold`), the start grace and its backoff (`--start-grace`), and the backoff alone
|
|
116
|
+
* (`--start-backoff-max`, the grace then stays at `--start-grace`).
|
|
117
|
+
*/
|
|
118
|
+
const DURATION_FLAGS = {
|
|
119
|
+
"heartbeat-ms": { bareMs: true },
|
|
120
|
+
"lease-expiry-ms": { bareMs: true },
|
|
121
|
+
"lease-margin-ms": { bareMs: true },
|
|
122
|
+
"park-threshold": { zeroOff: true },
|
|
123
|
+
"park-idle": {},
|
|
124
|
+
"drain-timeout": {},
|
|
125
|
+
"stop-timeout": {},
|
|
126
|
+
"control-timeout": {},
|
|
127
|
+
"lease-expiry": {},
|
|
128
|
+
"stonith-timeout": {},
|
|
129
|
+
"start-grace": { zeroOff: true },
|
|
130
|
+
"start-backoff-max": { zeroOff: true },
|
|
131
|
+
"token-grace": {},
|
|
132
|
+
every: {},
|
|
133
|
+
};
|
|
134
|
+
/** A duration flag's value in milliseconds, by the flag's rule; undefined when the flag is absent. */
|
|
135
|
+
function durationFlag(values, name) {
|
|
136
|
+
const value = values[name];
|
|
137
|
+
return typeof value === "string" ? parseDuration(value, DURATION_FLAGS[name]) : undefined;
|
|
98
138
|
}
|
|
99
139
|
function parse(argv, options) {
|
|
140
|
+
let values;
|
|
100
141
|
try {
|
|
101
|
-
|
|
142
|
+
values = parseArgs({ args: argv, options, allowPositionals: false, strict: true }).values;
|
|
102
143
|
}
|
|
103
144
|
catch (err) {
|
|
104
145
|
throw new UsageError(err.message);
|
|
105
146
|
}
|
|
147
|
+
// Every duration flag given is checked here, before the command does anything with any of them.
|
|
148
|
+
for (const name of Object.keys(DURATION_FLAGS))
|
|
149
|
+
durationFlag(values, name);
|
|
150
|
+
return values;
|
|
106
151
|
}
|
|
107
152
|
/** A user or group name or number, as a number (from /etc/passwd or /etc/group). */
|
|
108
153
|
function idOf(value, file, fallback) {
|
|
@@ -206,7 +251,7 @@ async function runInstance(values) {
|
|
|
206
251
|
const onSigterm = str(values["on-sigterm"], "resume");
|
|
207
252
|
if (onSigterm !== "resume" && onSigterm !== "pause")
|
|
208
253
|
throw new UsageError("--on-sigterm is resume or pause");
|
|
209
|
-
const ms = (name) => (
|
|
254
|
+
const ms = (name) => durationFlag(values, name);
|
|
210
255
|
const lease = { heartbeatMs: ms("heartbeat-ms"), expiryMs: ms("lease-expiry-ms"), marginMs: ms("lease-margin-ms") };
|
|
211
256
|
const parkMs = ms("park-threshold") || undefined;
|
|
212
257
|
const drainMs = ms("drain-timeout") ?? 0;
|
|
@@ -400,7 +445,7 @@ export function instanceRunArgs(values) {
|
|
|
400
445
|
return [...(typeof values.app === "string" ? ["--app", resolve(values.app)] : []), ...(values["run-arg"] ?? [])];
|
|
401
446
|
}
|
|
402
447
|
/** `--park-threshold` for either driver: absent keeps the driver's default, 0 turns parking off. */
|
|
403
|
-
const parkThreshold = (values) => values["park-threshold"] === undefined ? {} : { parkThresholdMs:
|
|
448
|
+
const parkThreshold = (values) => values["park-threshold"] === undefined ? {} : { parkThresholdMs: durationFlag(values, "park-threshold") || null };
|
|
404
449
|
function driverFrom(values) {
|
|
405
450
|
const env = Object.fromEntries((values.env ?? []).map((kv) => {
|
|
406
451
|
const i = kv.indexOf("=");
|
|
@@ -427,7 +472,7 @@ function driverFrom(values) {
|
|
|
427
472
|
dockerArgs: values["docker-arg"] ?? [],
|
|
428
473
|
apparmor: apparmor === "none" ? false : apparmor,
|
|
429
474
|
note: (line) => emit({ event: "docker", note: line }),
|
|
430
|
-
stopTimeoutMs: values["stop-timeout"] ?
|
|
475
|
+
stopTimeoutMs: values["stop-timeout"] ? durationFlag(values, "stop-timeout") : undefined,
|
|
431
476
|
...parkThreshold(values),
|
|
432
477
|
});
|
|
433
478
|
}
|
|
@@ -443,7 +488,7 @@ function driverFrom(values) {
|
|
|
443
488
|
user: str(values.user),
|
|
444
489
|
group: str(values.group),
|
|
445
490
|
runArgs,
|
|
446
|
-
stopTimeoutMs: values["stop-timeout"] ?
|
|
491
|
+
stopTimeoutMs: values["stop-timeout"] ? durationFlag(values, "stop-timeout") : undefined,
|
|
447
492
|
restart: values["no-restart"] ? false : undefined,
|
|
448
493
|
logDir: str(values["log-dir"]),
|
|
449
494
|
archil: str(values.archil),
|
|
@@ -476,7 +521,7 @@ async function supervise(values) {
|
|
|
476
521
|
if (docker && (typeof values.image !== "string" || !values.image))
|
|
477
522
|
throw new UsageError("--host docker needs --image");
|
|
478
523
|
const disk = await cliControl(values);
|
|
479
|
-
const controlTimeoutMs = values["control-timeout"] ?
|
|
524
|
+
const controlTimeoutMs = values["control-timeout"] ? durationFlag(values, "control-timeout") : CONTROL_TIMEOUT_MS;
|
|
480
525
|
if (values.check) {
|
|
481
526
|
const report = await checkHost({
|
|
482
527
|
control: disk,
|
|
@@ -506,10 +551,10 @@ async function supervise(values) {
|
|
|
506
551
|
const host = driverFrom(values);
|
|
507
552
|
const opts = {
|
|
508
553
|
control: disk,
|
|
509
|
-
leaseExpiryMs: values["lease-expiry"] ?
|
|
510
|
-
stonithTimeoutMs: values["stonith-timeout"] ?
|
|
511
|
-
startGraceMs: values
|
|
512
|
-
startBackoffMaxMs: values
|
|
554
|
+
leaseExpiryMs: values["lease-expiry"] ? durationFlag(values, "lease-expiry") : undefined,
|
|
555
|
+
stonithTimeoutMs: values["stonith-timeout"] ? durationFlag(values, "stonith-timeout") : undefined,
|
|
556
|
+
startGraceMs: durationFlag(values, "start-grace"),
|
|
557
|
+
startBackoffMaxMs: durationFlag(values, "start-backoff-max"),
|
|
513
558
|
tokenTtl: str(values["token-ttl"]),
|
|
514
559
|
tokenPrefix: str(values["token-prefix"]),
|
|
515
560
|
demand: Boolean(values.demand),
|
|
@@ -538,7 +583,7 @@ async function supervise(values) {
|
|
|
538
583
|
prefix: str(values["token-prefix"], TOKEN_PREFIX),
|
|
539
584
|
runs: values.id ? refs.map((x) => x.id) : undefined,
|
|
540
585
|
expired: Boolean(values["sweep-tokens"]),
|
|
541
|
-
graceMs: values["token-grace"] ?
|
|
586
|
+
graceMs: values["token-grace"] ? durationFlag(values, "token-grace") : undefined,
|
|
542
587
|
}).catch((e) => ({ removed: [], failed: [{ identifier: "*", error: e.message }] }));
|
|
543
588
|
if (r.removed.length || r.failed.length)
|
|
544
589
|
emit({ event: "token-sweep", ...r });
|
|
@@ -547,7 +592,7 @@ async function supervise(values) {
|
|
|
547
592
|
await tick();
|
|
548
593
|
return failures ? 1 : 0;
|
|
549
594
|
}
|
|
550
|
-
const every =
|
|
595
|
+
const every = durationFlag(values, "every");
|
|
551
596
|
let stopped = false;
|
|
552
597
|
process.once("SIGTERM", () => (stopped = true));
|
|
553
598
|
process.once("SIGINT", () => (stopped = true));
|
package/dist/env.js
CHANGED
|
@@ -510,6 +510,9 @@ export function archilEnv(claim, options = {}) {
|
|
|
510
510
|
if (runAs && ![runAs.uid, runAs.gid].every((n) => Number.isSafeInteger(n) && n >= 0)) {
|
|
511
511
|
throw new EnvError("INVALID_RUN_AS", `runAs needs a numeric uid and gid, got ${JSON.stringify(runAs)}`);
|
|
512
512
|
}
|
|
513
|
+
if (runAs?.uid === 0) {
|
|
514
|
+
throw new EnvError("INVALID_RUN_AS", "runAs names root (uid 0): the agent's commands would run as the owner of run.json, the store and the archil daemon");
|
|
515
|
+
}
|
|
513
516
|
const darwin = process.platform === "darwin";
|
|
514
517
|
const noNewPrivs = options.noNewPrivs ?? !darwin;
|
|
515
518
|
const setpriv = noNewPrivs || runAs ? requireSetpriv(options.setprivPath ?? SETPRIV) : undefined;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
import type { StartAttempt } from "../supervise.js";
|
|
2
|
+
/** A name an environment variable can have. */
|
|
3
|
+
export declare const ENV_NAME: RegExp;
|
|
4
|
+
/**
|
|
5
|
+
* Whether `name` is a secret's name: one of its `_`-separated segments, in any case, is KEY, TOKEN, SECRET, PASSWORD,
|
|
6
|
+
* PASSWD, CREDENTIAL or CREDENTIALS. Segments, not suffixes, so MODAL_TOKEN_ID is one too; CACHE_KEY_PREFIX is refused
|
|
7
|
+
* as well, and renamed, which is the cheaper error. A start's environment is readable in the instance, so a secret never
|
|
8
|
+
* goes there.
|
|
9
|
+
*/
|
|
10
|
+
export declare function keyShaped(name: string): boolean;
|
|
2
11
|
/** The start's extra environment, checked: PATH, PDA_HOLDER, a name that is not a variable name, or a value with a NUL, is `refuse`'s error. */
|
|
3
12
|
export declare function attemptEnv(attempt: StartAttempt | undefined, refuse: (message: string) => Error): Record<string, string>;
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/** A name an environment variable can have. */
|
|
2
|
+
export const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2
3
|
const DRIVER_KEYS = new Set(["PATH", "PDA_HOLDER"]);
|
|
4
|
+
const KEY_SEGMENTS = new Set(["KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL", "CREDENTIALS"]);
|
|
5
|
+
/**
|
|
6
|
+
* Whether `name` is a secret's name: one of its `_`-separated segments, in any case, is KEY, TOKEN, SECRET, PASSWORD,
|
|
7
|
+
* PASSWD, CREDENTIAL or CREDENTIALS. Segments, not suffixes, so MODAL_TOKEN_ID is one too; CACHE_KEY_PREFIX is refused
|
|
8
|
+
* as well, and renamed, which is the cheaper error. A start's environment is readable in the instance, so a secret never
|
|
9
|
+
* goes there.
|
|
10
|
+
*/
|
|
11
|
+
export function keyShaped(name) {
|
|
12
|
+
return name.split("_").some((segment) => KEY_SEGMENTS.has(segment.toUpperCase()));
|
|
13
|
+
}
|
|
3
14
|
/** The start's extra environment, checked: PATH, PDA_HOLDER, a name that is not a variable name, or a value with a NUL, is `refuse`'s error. */
|
|
4
15
|
export function attemptEnv(attempt, refuse) {
|
|
5
16
|
const env = attempt?.env ?? {};
|
package/dist/hosts/daytona.d.ts
CHANGED
|
@@ -158,7 +158,7 @@ export interface DaytonaHostOptions {
|
|
|
158
158
|
export declare function sandboxStatus(state: string | undefined): HostStatus | "check";
|
|
159
159
|
/** A Daytona-safe box name for a run: lowercase, at most 63 characters, unique per call. */
|
|
160
160
|
export declare function sandboxName(prefix: string, runId: string, now: number): string;
|
|
161
|
-
export declare function daytonaHost(opts: DaytonaHostOptions): HostDriver & Required<Pick<HostDriver, "kill" | "kills">> & {
|
|
161
|
+
export declare function daytonaHost(opts: DaytonaHostOptions): HostDriver & Required<Pick<HostDriver, "kill" | "kills" | "instanceOf">> & {
|
|
162
162
|
readonly fleet: string;
|
|
163
163
|
readonly mountRoot: string;
|
|
164
164
|
};
|
package/dist/hosts/daytona.js
CHANGED
|
@@ -293,6 +293,8 @@ export function daytonaHost(opts) {
|
|
|
293
293
|
fleet,
|
|
294
294
|
mountRoot,
|
|
295
295
|
kills: "box",
|
|
296
|
+
provider: "daytona",
|
|
297
|
+
instanceOf: (h) => (h.driver === "daytona" && typeof h.sandboxId === "string" ? `daytona\0${h.sandboxId}` : undefined),
|
|
296
298
|
async start(ref, mountToken, attempt) {
|
|
297
299
|
runPath(ref.id);
|
|
298
300
|
if (!mountToken)
|
package/dist/hosts/docker.d.ts
CHANGED
|
@@ -95,7 +95,7 @@ export type ContainerInfo = {
|
|
|
95
95
|
startedAt: string | null;
|
|
96
96
|
finishedAt: string | null;
|
|
97
97
|
};
|
|
98
|
-
export declare function dockerHost(opts?: DockerHostOptions): HostDriver & Required<Pick<HostDriver, "kill" | "kills">> & {
|
|
98
|
+
export declare function dockerHost(opts?: DockerHostOptions): HostDriver & Required<Pick<HostDriver, "kill" | "kills" | "instanceOf">> & {
|
|
99
99
|
readonly fleet: string;
|
|
100
100
|
readonly mountRoot: string;
|
|
101
101
|
describe(handle: HostHandle): Promise<ContainerInfo | null>;
|
package/dist/hosts/docker.js
CHANGED
|
@@ -272,6 +272,7 @@ export function dockerHost(opts = {}) {
|
|
|
272
272
|
fleet,
|
|
273
273
|
mountRoot,
|
|
274
274
|
kills: "box",
|
|
275
|
+
instanceOf: (h) => (h.driver === "docker" && typeof h.daemon === "string" && typeof h.name === "string" ? `docker\0${h.daemon}\0${h.name}` : undefined),
|
|
275
276
|
async start(ref, token, attemptInfo) {
|
|
276
277
|
runPath(ref.id);
|
|
277
278
|
if (!opts.image)
|
|
@@ -66,7 +66,7 @@ export declare function currentBootId(): string;
|
|
|
66
66
|
export declare function parseShow(text: string): Record<string, string>;
|
|
67
67
|
/** A unit's state as a host status: loaded and active (or restarting) is running; unloaded is gone. */
|
|
68
68
|
export declare function unitStatus(show: Record<string, string>): HostStatus;
|
|
69
|
-
export declare function localHost(opts?: LocalHostOptions): HostDriver & Required<Pick<HostDriver, "kill" | "kills">> & {
|
|
69
|
+
export declare function localHost(opts?: LocalHostOptions): HostDriver & Required<Pick<HostDriver, "kill" | "kills" | "instanceOf">> & {
|
|
70
70
|
readonly hostName: string;
|
|
71
71
|
readonly mountRoot: string;
|
|
72
72
|
};
|