@parcha/pi-durable-disk 0.0.0-stage → 0.1.0-beta.13

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +329 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +695 -2
  5. package/bin/archil-scoped +165 -0
  6. package/dist/app.d.ts +40 -0
  7. package/dist/app.js +47 -0
  8. package/dist/claim.d.ts +222 -0
  9. package/dist/claim.js +722 -0
  10. package/dist/cli.d.ts +64 -0
  11. package/dist/cli.js +664 -0
  12. package/dist/commands.d.ts +67 -0
  13. package/dist/commands.js +299 -0
  14. package/dist/confine.d.ts +91 -0
  15. package/dist/confine.js +341 -0
  16. package/dist/env.d.ts +129 -0
  17. package/dist/env.js +608 -0
  18. package/dist/errors.d.ts +47 -0
  19. package/dist/errors.js +55 -0
  20. package/dist/fork.d.ts +41 -0
  21. package/dist/fork.js +181 -0
  22. package/dist/hosts/daytona-launch.d.ts +52 -0
  23. package/dist/hosts/daytona-launch.js +270 -0
  24. package/dist/hosts/daytona.d.ts +147 -0
  25. package/dist/hosts/daytona.js +368 -0
  26. package/dist/hosts/docker.d.ts +103 -0
  27. package/dist/hosts/docker.js +362 -0
  28. package/dist/hosts/local-host.d.ts +71 -0
  29. package/dist/hosts/local-host.js +356 -0
  30. package/dist/index.d.ts +16 -0
  31. package/dist/index.js +17 -0
  32. package/dist/lease.d.ts +7 -0
  33. package/dist/lease.js +6 -0
  34. package/dist/local-run.d.ts +10 -0
  35. package/dist/local-run.js +63 -0
  36. package/dist/park.d.ts +92 -0
  37. package/dist/park.js +229 -0
  38. package/dist/run.d.ts +290 -0
  39. package/dist/run.js +785 -0
  40. package/dist/serve.d.ts +101 -0
  41. package/dist/serve.js +393 -0
  42. package/dist/status.d.ts +123 -0
  43. package/dist/status.js +264 -0
  44. package/dist/store.d.ts +103 -0
  45. package/dist/store.js +217 -0
  46. package/dist/supervise.d.ts +340 -0
  47. package/dist/supervise.js +528 -0
  48. package/dist/watchdog.d.ts +39 -0
  49. package/dist/watchdog.js +137 -0
  50. package/package.json +87 -4
@@ -0,0 +1,165 @@
1
+ #!/bin/sh
2
+ # archil, with `mount` started in its own transient scope (<unit>-fuse-<n>.scope) when the caller runs inside a system
3
+ # service, so the FUSE daemon that `archil mount` forks lives outside the instance unit's control group: stopping or
4
+ # restarting the instance (KillMode=control-group) kills every command it started but never its mount, and an
5
+ # in-place restart finds its claim still held. Any other verb, and any caller outside a system service, runs
6
+ # archil unchanged. Run through sudo; install it root-owned, since root executes it.
7
+ #
8
+ # `mount` reads its token as one line on stdin and hands it to archil only in the environment of that exec. The token
9
+ # is never an argument and never in an environment sudo sees: sudo logs every variable it preserves. An empty line or
10
+ # no line refuses the mount.
11
+ #
12
+ # `retire <root>/runs/<run> <tag>` is for a kernel that refuses every unmount of a FUSE mount it still lists (Sysbox, as
13
+ # Daytona runs sandboxes: umount2 fails with ENOENT). It moves the run's fuse.archil mount aside to
14
+ # <root>/.released/<run>-<tag> and SIGKILLs its daemon, the process whose argument vector is this archil's `mount`
15
+ # naming exactly that mountpoint, so the run's path is free for its next mount. The caller checks the delegation in
16
+ # first; a dead mount has none left. Nothing else is moved or signalled.
17
+ #
18
+ # `stray <root>/runs/<run> <tag>` is for a run's mountpoint that is no mount and not empty (files a process wrote by
19
+ # path after the run's mount vanished), which archil refuses to mount over. The directory is moved, whole, to
20
+ # <root>/.stray/<run>-<tag> and replaced by an empty one with its owner and mode, made inside .stray first and renamed
21
+ # into place. Nothing in it is read, copied or brought into the run; the output names only the entry counts.
22
+ #
23
+ # `stale <root>/runs/<run>` is for an archil daemon left on a run's mountpoint with no mount in the table (a client that
24
+ # dropped its mount after an authentication failure, or whose mount was lazily unmounted while a process still held a
25
+ # reference into it, keeps its process and the mountpoint's control socket), which makes every later mount there fail
26
+ # with "an older Archil process is still running". It SIGKILLs the daemons whose argument vector is this archil's `mount`
27
+ # naming exactly that mountpoint, and refuses while a mount is listed there (a live mount's daemon is never stale).
28
+ #
29
+ # The run's user may own <root>, so root never follows a link there: <root> and <root>/runs must be their own real
30
+ # paths, the holding directory (.released or .stray) a real directory owned by root (made 0700 when absent), and the
31
+ # destination new (never one that exists).
32
+ set -eu
33
+ ARCHIL=/usr/bin/archil
34
+ MOUNT=/usr/bin/mount
35
+ MOUNTS=/proc/self/mounts
36
+ PROC=/proc
37
+ KILL=kill
38
+ OWNER=0
39
+
40
+ listed() { awk -v m="$1" -v t="$2" '$2 == m && (t == "" || $3 == t) { f = 1 } END { exit !f }' "$MOUNTS"; }
41
+
42
+ # The run's path and, for a verb that moves it, the tag: a plain <root>/runs/<run>, a one-segment tag, no link in <root>
43
+ # or <root>/runs. Sets mp, tag and root.
44
+ run_path() {
45
+ mp=$1 tag=${2-}
46
+ case "$mp" in */./*|*/../*|*/.|*/..|*//*) echo "archil-scoped: $mp is not a plain path" >&2; exit 2 ;; esac
47
+ printf '%s\n' "$mp" | grep -Eqx '/[A-Za-z0-9._/-]+/runs/[A-Za-z0-9][A-Za-z0-9._-]{0,127}' || { echo "archil-scoped: $mp is not <root>/runs/<run>" >&2; exit 2; }
48
+ [ "$#" -lt 2 ] || printf '%s\n' "$tag" | grep -Eqx '[A-Za-z0-9][A-Za-z0-9._-]{0,63}' || { echo "archil-scoped: tag $tag is not one safe path segment" >&2; exit 2; }
49
+ root=${mp%/runs/*}
50
+ for p in "$root" "$root/runs"; do
51
+ [ "$(realpath -e "$p" 2>/dev/null)" = "$p" ] || { echo "archil-scoped: $p is not its own real path (a link, or missing)" >&2; exit 2; }
52
+ done
53
+ }
54
+
55
+ # <root>/<name> as a real directory owned by root, made 0700 when absent. Sets held.
56
+ holding() {
57
+ held="$root/$1"
58
+ if [ ! -e "$held" ] && [ ! -L "$held" ]; then
59
+ mkdir -m 0700 "$held" || { echo "archil-scoped: cannot create $held" >&2; exit 6; }
60
+ fi
61
+ if [ -L "$held" ] || [ ! -d "$held" ] || [ "$(stat -c %u "$held")" != "$OWNER" ]; then
62
+ echo "archil-scoped: $held is not a real directory owned by root" >&2
63
+ exit 6
64
+ fi
65
+ }
66
+
67
+ # The archil daemons of $mp: processes whose argument vector is this archil's `mount` naming exactly $mp. Sets pids.
68
+ # One grep over every cmdline finds the few that hold $mp as a whole argument (a per-process pipeline costs seconds on a
69
+ # host with thousands of processes); each of those is then matched exactly.
70
+ daemons_of() {
71
+ pids=
72
+ cands=$(grep -zlFx -- "$mp" "$PROC"/[0-9]*/cmdline 2>/dev/null) || true
73
+ while IFS= read -r f; do
74
+ [ -n "$f" ] || continue
75
+ if tr '\0' '\n' < "$f" 2>/dev/null | awk -v a="$ARCHIL" -v m="$mp" 'NR == 1 { x = ($0 == a) } NR == 2 { y = ($0 == "mount") } NR > 2 && $0 == m { z = 1 } END { exit !(x && y && z) }'; then
76
+ d=${f%/cmdline}
77
+ pids="$pids ${d##*/}"
78
+ fi
79
+ done <<EOF
80
+ $cands
81
+ EOF
82
+ }
83
+
84
+ # SIGKILL $pids and wait up to 5 s for them to be gone.
85
+ kill_daemons() {
86
+ [ -n "$pids" ] || return 0
87
+ $KILL -KILL $pids 2>/dev/null || true
88
+ n=0 left=$pids
89
+ while [ -n "$left" ] && [ "$n" -lt 50 ]; do
90
+ sleep 0.1
91
+ n=$((n + 1))
92
+ left=
93
+ for p in $pids; do if [ -d "$PROC/$p" ]; then left="$left $p"; fi; done
94
+ done
95
+ [ -z "$left" ] || { echo "archil-scoped: archil daemon$left still alive" >&2; exit 5; }
96
+ }
97
+
98
+ retire() {
99
+ run_path "$1" "$2"
100
+ listed "$mp" fuse.archil || { echo "archil-scoped: no fuse.archil mount at $mp" >&2; exit 3; }
101
+ holding .released
102
+ dest="$held/${mp##*/}-$tag"
103
+ daemons_of
104
+ mkdir -m 0700 "$dest" 2>/dev/null || { echo "archil-scoped: $dest exists or cannot be made" >&2; exit 6; }
105
+ [ "$(realpath -e "$dest")" = "$dest" ] || { echo "archil-scoped: $dest is not its own real path" >&2; exit 6; }
106
+ "$MOUNT" --move "$mp" "$dest" || { echo "archil-scoped: mount --move $mp $dest failed" >&2; exit 4; }
107
+ if listed "$mp" ""; then echo "archil-scoped: $mp is still mounted after the move" >&2; exit 4; fi
108
+ kill_daemons
109
+ echo "archil-scoped: moved $mp to $dest; killed:${pids:- none}"
110
+ }
111
+
112
+ stale() {
113
+ run_path "$1"
114
+ if listed "$mp" ""; then echo "archil-scoped: $mp is mounted; its daemon is not stale" >&2; exit 3; fi
115
+ daemons_of
116
+ if [ -z "$pids" ]; then echo "archil-scoped: no archil daemon for $mp"; return; fi
117
+ if listed "$mp" ""; then echo "archil-scoped: $mp was mounted meanwhile; its daemon is not stale" >&2; exit 3; fi
118
+ kill_daemons
119
+ echo "archil-scoped: killed stale archil daemon(s) of $mp:$pids"
120
+ }
121
+
122
+ stray() {
123
+ run_path "$1" "$2"
124
+ if [ -L "$mp" ] || [ ! -d "$mp" ] || [ "$(realpath -e "$mp")" != "$mp" ]; then
125
+ echo "archil-scoped: $mp is not a real directory" >&2
126
+ exit 2
127
+ fi
128
+ if listed "$mp" ""; then echo "archil-scoped: $mp is a mount" >&2; exit 3; fi
129
+ entries=$(find "$mp" -mindepth 1 -maxdepth 1 | wc -l)
130
+ if [ "$entries" -eq 0 ]; then echo "archil-scoped: $mp is empty; nothing moved"; return; fi
131
+ total=$(find "$mp" -mindepth 1 | wc -l)
132
+ holding .stray
133
+ dest="$held/${mp##*/}-$tag"
134
+ fresh="$held/.fresh-${mp##*/}-$tag"
135
+ for p in "$dest" "$fresh"; do
136
+ if [ -e "$p" ] || [ -L "$p" ]; then echo "archil-scoped: $p exists" >&2; exit 6; fi
137
+ done
138
+ # Both renames stay on one filesystem: mv would otherwise copy, as root, whatever the directory holds.
139
+ [ "$(stat -c %d "$root/runs")" = "$(stat -c %d "$held")" ] || { echo "archil-scoped: $held is on another filesystem than $root/runs" >&2; exit 6; }
140
+ set -- $(stat -c '%u %g %a' "$mp")
141
+ { mkdir -m 0700 "$fresh" && chown "$1:$2" "$fresh" && chmod "$3" "$fresh"; } || { echo "archil-scoped: cannot prepare $fresh" >&2; exit 6; }
142
+ mv -T "$mp" "$dest" || { rmdir "$fresh"; echo "archil-scoped: moving $mp to $dest failed" >&2; exit 4; }
143
+ mv -T "$fresh" "$mp" || { echo "archil-scoped: $mp could not be replaced by an empty directory" >&2; exit 4; }
144
+ echo "archil-scoped: moved stray $mp to $dest; entries=$entries total=$total"
145
+ }
146
+
147
+ case "${1:-}" in
148
+ retire | stray)
149
+ [ "$#" -eq 3 ] || { echo "usage: archil-scoped $1 <root>/runs/<run> <tag>" >&2; exit 2; }
150
+ "$1" "$2" "$3"
151
+ exit 0
152
+ ;;
153
+ stale)
154
+ [ "$#" -eq 2 ] || { echo "usage: archil-scoped stale <root>/runs/<run>" >&2; exit 2; }
155
+ stale "$2"
156
+ exit 0
157
+ ;;
158
+ esac
159
+ [ "${1:-}" = mount ] || exec "$ARCHIL" "$@"
160
+ token=
161
+ IFS= read -r token || true
162
+ [ -n "$token" ] || { echo "archil-scoped: no mount token on stdin" >&2; exit 2; }
163
+ unit=$(sed -n 's|^0::/system\.slice/\(.*/\)\{0,1\}\([A-Za-z0-9:_.-]*\)\.service$|\2|p' /proc/self/cgroup)
164
+ [ -n "$unit" ] || ARCHIL_MOUNT_TOKEN="$token" exec "$ARCHIL" "$@"
165
+ ARCHIL_MOUNT_TOKEN="$token" exec /usr/bin/systemd-run --scope --quiet --collect --unit="$unit-fuse-$(date +%s%N)" -- "$ARCHIL" "$@"
package/dist/app.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ import type { Harness } from "@earendil-works/pi-durable";
2
+ import type { RunRef } from "./claim.js";
3
+ import { PdaError } from "./errors.js";
4
+ import type { DurableRun, OpenDurableRunOptions } from "./run.js";
5
+ /** Where the run lives on this host; known before it opens, so the app can build paths into its options. */
6
+ export interface AppContext {
7
+ readonly ref: RunRef;
8
+ readonly root: string;
9
+ readonly work: string;
10
+ readonly store: string;
11
+ }
12
+ /** pi's HarnessOptions without `env` (registry, models, settings, conversationCreated, now, onReport), plus the hooks below. */
13
+ export type AppOptions = OpenDurableRunOptions["harness"] & {
14
+ /**
15
+ * Awaited once per incarnation after the Harness is open and before it resumes (`OpenDurableRunOptions.beforeResume`):
16
+ * no resumed task takes a step until it settles. A rejection fails the open (exit 1, retried).
17
+ */
18
+ beforeResume?: OpenDurableRunOptions["beforeResume"];
19
+ /**
20
+ * Called once per incarnation after the run is open and resumed; `run.generation` tells a first start from a resume.
21
+ * Work that must keep the instance up belongs in pi's tasks and tools: parking sees only those.
22
+ */
23
+ onOpen?(run: DurableRun): void | Promise<void>;
24
+ /** pi's `root()` options for when `serve` creates the root conversation (its agent: model, extensions). */
25
+ root?: Parameters<Harness["root"]>[1];
26
+ /** Record a parking run's wake (park.ts `ParkOptions.wake`); default run.json `sleeping` with `wakeAt`. */
27
+ wake?(run: DurableRun, at: number | null): Promise<void>;
28
+ };
29
+ /** What `--app` names: `export default async (ctx) => ({ registry, models, onOpen })`. */
30
+ export type AppModule = (ctx: AppContext) => AppOptions | Promise<AppOptions>;
31
+ /**
32
+ * The app module is missing, does not load, has no default export function, throws, returns no options, or its
33
+ * `onOpen` failed. Exit 1: not terminal, so the unit retries within systemd's start limit.
34
+ */
35
+ export declare class AppError extends PdaError {
36
+ constructor(message: string, options?: {
37
+ cause?: unknown;
38
+ });
39
+ }
40
+ export declare function loadApp(path: string, ctx: AppContext): Promise<AppOptions>;
package/dist/app.js ADDED
@@ -0,0 +1,47 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // The app an instance hosts (`pi-durable-disk run --app <module>`): an ES module whose default export is a function
10
+ // of where the run lives that returns pi's Harness options (without `env`, which the run builds on its claim), an
11
+ // optional `beforeResume` for what must settle before the run resumes, an optional `onOpen` for the app to submit or
12
+ // resume work once the run is open and resumed, how `serve` creates the root conversation, and how a parking run records
13
+ // its wake.
14
+ import { resolve } from "node:path";
15
+ import { pathToFileURL } from "node:url";
16
+ import { PdaError } from "./errors.js";
17
+ /**
18
+ * The app module is missing, does not load, has no default export function, throws, returns no options, or its
19
+ * `onOpen` failed. Exit 1: not terminal, so the unit retries within systemd's start limit.
20
+ */
21
+ export class AppError extends PdaError {
22
+ constructor(message, options = {}) {
23
+ super("APP_FAILED", message, options);
24
+ }
25
+ }
26
+ export async function loadApp(path, ctx) {
27
+ let mod;
28
+ try {
29
+ mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(resolve(path)).href)));
30
+ }
31
+ catch (err) {
32
+ throw new AppError(`cannot load the app module ${path}: ${err.message}`, { cause: err });
33
+ }
34
+ if (typeof mod.default !== "function")
35
+ throw new AppError(`the app module ${path} has no default export function`);
36
+ let options;
37
+ try {
38
+ options = await mod.default(ctx);
39
+ }
40
+ catch (err) {
41
+ throw new AppError(`the app module ${path} threw: ${err.message}`, { cause: err });
42
+ }
43
+ if (typeof options !== "object" || options === null || !options.registry || !options.models) {
44
+ throw new AppError(`the app module ${path} returned no Harness options (registry and models)`);
45
+ }
46
+ return options;
47
+ }
@@ -0,0 +1,222 @@
1
+ import type { Delegation } from "disk";
2
+ export type RunRef = {
3
+ disk: string;
4
+ region: string;
5
+ id: string;
6
+ };
7
+ /** The control-plane calls the claim uses; an archil `Disk` (SDK `disk` 1.7.0) satisfies it. Supervisor side only. */
8
+ export interface ControlApi {
9
+ putObject(key: string, body: string, options: {
10
+ uid: number;
11
+ gid: number;
12
+ mode: number;
13
+ }): Promise<unknown>;
14
+ addUser(user: {
15
+ type: "token";
16
+ nickname: string;
17
+ ttl: string;
18
+ oneUse: boolean;
19
+ }): Promise<{
20
+ identifier?: string;
21
+ token?: string;
22
+ }>;
23
+ removeUser(type: "token", identifier: string): Promise<void>;
24
+ listDelegations(): Promise<Delegation[]>;
25
+ revokeDelegation(delegation: Pick<Delegation, "clientId" | "inodeId">): Promise<void>;
26
+ /**
27
+ * `Disk.exec`: a shell command in a container with the disk mounted in shared mode, at the disk root. The claim uses it
28
+ * only to resolve `runs/<id>` to its inode when the control API lists a delegation without a path. Without it, such a
29
+ * delegation cannot be attributed, and `findDelegations` fails rather than read the run as unheld.
30
+ */
31
+ exec?(command: string): Promise<{
32
+ exitCode: number;
33
+ stdout: string;
34
+ stderr?: string;
35
+ }>;
36
+ }
37
+ /** How this host runs the archil client. `archil mount`, `sync`, `delegations` and `unmount` all need root. */
38
+ export interface ArchilHost {
39
+ /**
40
+ * The archil command: `bin/archil-scoped` or a root-owned copy of it (default: this package's). Its `mount` takes the
41
+ * token as one line on stdin; plain `/usr/bin/archil` does not, so a mount through it fails unauthenticated.
42
+ */
43
+ archil?: string;
44
+ fusermount?: string;
45
+ /** The sudo binary, or false when the process is root. Default: /usr/bin/sudo unless uid 0. */
46
+ sudo?: string | false;
47
+ procMounts?: string;
48
+ /** The process table the claim reads to find a stale archil daemon on the run's mountpoint. Default /proc. */
49
+ proc?: string;
50
+ /** `staleGrace`: how long a daemon left on an unmounted mountpoint may take to exit by itself before it is killed. */
51
+ timeoutMs?: {
52
+ mount?: number;
53
+ sync?: number;
54
+ unmount?: number;
55
+ cli?: number;
56
+ staleGrace?: number;
57
+ };
58
+ /** File access on the mount; replaceable so tests can inject a dead mount (ENOTCONN) or a fence (EIO). */
59
+ fs?: {
60
+ stat?(path: string): Promise<unknown>;
61
+ persist?(path: string, data: string): Promise<void>;
62
+ };
63
+ }
64
+ export interface AcquireOptions {
65
+ ref: RunRef;
66
+ /** The mount token. It travels only on the archil wrapper's stdin; never in argv or an environment, never printed or written. */
67
+ token: string;
68
+ /** Default /mnt/archil; the claim mounts at `<mountRoot>/runs/<id>` so paths are identical on every host. */
69
+ mountRoot?: string;
70
+ /** `archil mount --force`: the takeover fallback when the control API cannot revoke. */
71
+ force?: boolean;
72
+ host?: ArchilHost;
73
+ }
74
+ export interface Claim {
75
+ readonly ref: RunRef;
76
+ readonly disk: string;
77
+ readonly root: string;
78
+ readonly work: string;
79
+ readonly store: string;
80
+ /** True when an in-place restart found its own live mount and kept it; no token was used. */
81
+ readonly reused: boolean;
82
+ /** Entries found in the mountpoint before mounting, moved aside whole and never brought into the run; else null. */
83
+ readonly stray?: {
84
+ to: string;
85
+ entries: number;
86
+ } | null;
87
+ readonly forced: boolean;
88
+ readonly timings: {
89
+ mountMs: number;
90
+ verifyMs: number;
91
+ };
92
+ readonly fenced: boolean;
93
+ /** Mark the claim lost (the store's facade calls this on an I/O-class error); later barriers throw at once. */
94
+ markFenced(cause?: unknown): void;
95
+ /**
96
+ * Where the run's environment records the commands it starts, so that the next open of this claim ends what a killed
97
+ * incarnation left running (`reapCommands`, before the Harness opens). Set by a claim that nothing else cleans up after,
98
+ * as a local run's; a mount's host ends them with its unit's cgroup or its container.
99
+ */
100
+ readonly commands?: string;
101
+ /** `archil sync`: every pending write on the mount is durable when it resolves. A failure fences the claim. */
102
+ barrier(): Promise<{
103
+ ms: number;
104
+ }>;
105
+ /**
106
+ * Barrier, then `archil unmount` (flush and check the delegation in); a dead mount is cleaned with `fusermount -u`.
107
+ * Where the kernel refuses every unmount of a FUSE mount with ENOENT (Sysbox, Daytona's sandboxes): `archil checkin`,
108
+ * then the mount is moved aside and its daemon killed (`moved`).
109
+ */
110
+ release(): Promise<{
111
+ via: Unmounted;
112
+ }>;
113
+ }
114
+ export declare const DEFAULT_MOUNT_ROOT = "/mnt/archil";
115
+ /** This package's `bin/archil-scoped`. A deployment installs a root-owned copy and passes its path as `archil`. */
116
+ export declare const ARCHIL_SCOPED: string;
117
+ /** The file the claim writes and fsyncs to prove it holds the delegation; written on every acquire. */
118
+ export declare const CLAIM_PROBE = ".claim";
119
+ /** How a mount left its path: unmounted by archil, by fusermount, moved aside (Sysbox), or there was none. */
120
+ export type Unmounted = "archil" | "fusermount" | "moved" | "none";
121
+ export declare function runPath(id: string): string;
122
+ /** PutObject of the `/`-terminated key; uid/gid are required or the directory is root:root. 409: held. */
123
+ export declare function createRunDir(control: ControlApi, id: string, owner: {
124
+ uid: number;
125
+ gid: number;
126
+ mode?: number;
127
+ }): Promise<void>;
128
+ /** A mount token's lifetime: one day, a Go duration (the control plane rejects "1d"). */
129
+ export declare const MOUNT_TOKEN_TTL = "24h";
130
+ export declare const TOKEN_PREFIX = "pda-";
131
+ /** `<prefix><run id>-g<attempt>-<base36 ms>`: the token user names the run and start attempt it was minted for. */
132
+ export declare function tokenNickname(runId: string, attempt: number, at?: number, prefix?: string): string;
133
+ /** The run, attempt and minting time a nickname from `tokenNickname` names; null for anything else. */
134
+ export declare function parseTokenNickname(nickname: string, prefix?: string): {
135
+ runId: string;
136
+ attempt: number;
137
+ at: number;
138
+ } | null;
139
+ /**
140
+ * A reusable mount token with a one-day TTL by default, minted per start attempt. Not single-use: the client
141
+ * re-authenticates at its background endpoint refresh, 5 min after the mount and every 5 min after, and the control plane
142
+ * rejects a spent single-use token for good, after which every write on the mount fails (client 0.8.42). A token
143
+ * whose TTL passes fences its mount the same way at the next refresh, so an instance never outlives its token by more
144
+ * than one refresh. The nickname is given, or built from `run` so the user can be traced back to its run.
145
+ */
146
+ export declare function mintMountToken(control: ControlApi, opts: {
147
+ nickname?: string;
148
+ run?: {
149
+ id: string;
150
+ attempt: number;
151
+ };
152
+ prefix?: string;
153
+ ttl?: string;
154
+ oneUse?: boolean;
155
+ now?: () => number;
156
+ }): Promise<{
157
+ token: string;
158
+ identifier: string;
159
+ nickname: string;
160
+ }>;
161
+ export declare function removeMountToken(control: ControlApi, identifier: string): Promise<void>;
162
+ /**
163
+ * Delegations on `runs/<id>` and anything under it; never a sibling or a parent. The control API resolves a delegation's
164
+ * path best-effort: it can list one with no path, live or orphaned, on a never-reused run. So when nothing matches by
165
+ * path and some entry has none, the pathless entries are attributed by inode (`pathlessResolver`): those on the inode
166
+ * `runs/<id>` names now are the run's. A pathless entry that cannot be attributed fails CONTROL_API_FAILED: an unseen
167
+ * holder must never read as none, or a caller starts over it.
168
+ */
169
+ export declare function findDelegations(control: Pick<ControlApi, "listDelegations" | "exec">, id: string, resolve?: PathlessResolver): Promise<Delegation[]>;
170
+ /** `findDelegations` over a listing the caller already holds; share `resolve` across the runs of one listing or pass. */
171
+ export declare function matchDelegations(all: readonly Delegation[], id: string, resolve: PathlessResolver): Promise<Delegation[]>;
172
+ /** The run id whose directory each inode is, or null when it is no run directory (deleted, or not under `runs/`). */
173
+ export type PathlessResolver = (pathless: readonly Delegation[]) => Promise<ReadonlyMap<number, string | null>>;
174
+ /**
175
+ * Attributes pathless delegations by inode: one `exec` maps every inode not seen before to the run directory that has
176
+ * it (`find runs -inum`, whose inode numbers are the control API's inode ids). Share one across a listing or a supervisor
177
+ * pass: it costs one exec per batch of new inodes, never one per run. The output names only the inodes asked for, so it
178
+ * stays small however many runs the disk holds. A failed exec fails this call and every later one, with no new exec.
179
+ * Inodes on no run directory are reported once on stderr: they block nothing, and only a revoke by client and inode
180
+ * removes them.
181
+ */
182
+ export declare function pathlessResolver(control: Pick<ControlApi, "exec">): PathlessResolver;
183
+ /**
184
+ * The delegations `held`'s clients also hold on their own private directories (`privateDirs`: inode to the client whose
185
+ * private directory it is). A killed client leaves them orphaned, and nothing ties them to a run: they block no run, but
186
+ * they pile up and resurface in listings. A client that holds anything else beyond `held` may hold another run, so none
187
+ * of its delegations is a companion.
188
+ */
189
+ export declare function companionsOf(all: readonly Delegation[], held: readonly Delegation[], privateDirs: ReadonlyMap<number, string>): Delegation[];
190
+ /**
191
+ * Revoke the companions of `held` (`companionsOf`): only for clients the caller is already revoking because it fences or
192
+ * deletes a run, never as a sweep. From `all` or a fresh listing. Best effort, and hygiene only: a companion blocks no
193
+ * run, so one left behind (or a failed listing or `exec`) costs a listing entry. Returns the ones revoked.
194
+ */
195
+ export declare function revokeCompanions(control: Pick<ControlApi, "listDelegations" | "revokeDelegation" | "exec">, held: readonly Delegation[], all?: readonly Delegation[]): Promise<Delegation[]>;
196
+ /**
197
+ * Revoke every delegation on the run through the control API, then its holders' private directories
198
+ * (`revokeCompanions`). The old holder's next fsync returns EIO. Returns the run's own delegations.
199
+ */
200
+ export declare function revoke(control: ControlApi, id: string): Promise<Delegation[]>;
201
+ /**
202
+ * Revoke what `revoke` revokes, skipping every revoke that fails: for cleanup, where deleting the tree next checks what
203
+ * is left. Only the listing (and a pathless resolution) can fail it. Returns the run's own delegations.
204
+ */
205
+ export declare function revokeBestEffort(control: Pick<ControlApi, "listDelegations" | "revokeDelegation" | "exec">, id: string): Promise<Delegation[]>;
206
+ /** Revoke through the control API, then mount plainly; if the API path fails, mount with `--force`. */
207
+ export declare function takeOver(control: ControlApi, opts: AcquireOptions): Promise<Claim>;
208
+ /**
209
+ * Unmount whatever the run has at `mountpoint`: `archil unmount` flushes and checks the delegation in; a dead mount
210
+ * (which `archil unmount` refuses) is cleaned with `fusermount -u`; a live mount refused with ENOENT (Sysbox) is checked
211
+ * in and moved aside. Any other live mount that refuses is never forced.
212
+ */
213
+ export declare function unmountClaim(mountpoint: string, host?: ArchilHost): Promise<Unmounted>;
214
+ /**
215
+ * Take the run's claim: mount `<disk>:/runs/<id>` exclusively at `<mountRoot>/runs/<id>` and verify it. A live mount of
216
+ * the same run already there is kept (in-place restart); a dead one is cleaned first. With no mount there, a stale archil
217
+ * daemon on the mountpoint is killed (`stale`) and entries in it are moved aside to `<mountRoot>/.stray` (`stray`) before
218
+ * the mount. Throws HeldError (exit 76) only when another client holds the delegation, FencedError (exit 75) when the
219
+ * verifying write is refused, MOUNTPOINT_NOT_EMPTY when the mountpoint cannot be emptied or archil still finds it not
220
+ * empty, MOUNT_FAILED (exit 1) for a daemon of this host that is still running there.
221
+ */
222
+ export declare function acquire(opts: AcquireOptions): Promise<Claim>;