@cotal-ai/seat 0.48.2 → 0.50.0
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/README.md +21 -4
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -2
- package/dist/client.js.map +1 -1
- package/dist/custodian.d.ts +8 -1
- package/dist/custodian.d.ts.map +1 -1
- package/dist/custodian.js +259 -29
- package/dist/custodian.js.map +1 -1
- package/dist/handle.d.ts +2 -1
- package/dist/handle.d.ts.map +1 -1
- package/dist/handle.js +9 -9
- package/dist/handle.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/launcher.d.ts +23 -1
- package/dist/launcher.d.ts.map +1 -1
- package/dist/launcher.js +101 -7
- package/dist/launcher.js.map +1 -1
- package/dist/protocol.d.ts +43 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +57 -2
- package/dist/protocol.js.map +1 -1
- package/dist/reap.d.ts +47 -0
- package/dist/reap.d.ts.map +1 -0
- package/dist/reap.js +182 -0
- package/dist/reap.js.map +1 -0
- package/dist/record.d.ts +29 -0
- package/dist/record.d.ts.map +1 -1
- package/dist/record.js +54 -2
- package/dist/record.js.map +1 -1
- package/dist/startup-confirm.d.ts +17 -0
- package/dist/startup-confirm.d.ts.map +1 -0
- package/dist/startup-confirm.js +42 -0
- package/dist/startup-confirm.js.map +1 -0
- package/package.json +5 -1
package/dist/reap.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { bootToken, processStartToken, readRecord, recordPath } from "./record.js";
|
|
4
|
+
import { RUN_MARKER_FLAG } from "./launcher.js";
|
|
5
|
+
import { unsupportedTransport } from "./protocol.js";
|
|
6
|
+
/** A pid's standing against a recorded start identity. `live` only when the process exists AND
|
|
7
|
+
* carries the recorded start token; a different token means the pid was reused by an unrelated
|
|
8
|
+
* process, which is never signalled. */
|
|
9
|
+
export function identityVerdict(pid, start) {
|
|
10
|
+
const now = processStartToken(pid);
|
|
11
|
+
return now !== undefined && now === start ? "live" : "gone";
|
|
12
|
+
}
|
|
13
|
+
function processGroupOf(pid) {
|
|
14
|
+
try {
|
|
15
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
16
|
+
const after = stat.lastIndexOf(") ");
|
|
17
|
+
if (after < 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
const pgrp = Number(stat.slice(after + 2).split(" ")[2]);
|
|
20
|
+
return Number.isInteger(pgrp) ? pgrp : undefined;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Every live pid whose process group is `pgid`. Bounded by the process table; read once per poll. */
|
|
27
|
+
function groupMembers(pgid) {
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const entry of readdirSync("/proc")) {
|
|
30
|
+
if (!/^\d+$/.test(entry))
|
|
31
|
+
continue;
|
|
32
|
+
const pid = Number(entry);
|
|
33
|
+
if (processGroupOf(pid) === pgid)
|
|
34
|
+
out.push(pid);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function signal(pid, sig) {
|
|
39
|
+
try {
|
|
40
|
+
process.kill(pid, sig);
|
|
41
|
+
return "signalled";
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (e.code === "ESRCH")
|
|
45
|
+
return "gone";
|
|
46
|
+
throw new Error(`cannot signal pid ${pid}: ${e.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function sleep(ms) {
|
|
50
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
51
|
+
}
|
|
52
|
+
async function until(predicate, timeoutMs) {
|
|
53
|
+
const deadline = Date.now() + timeoutMs;
|
|
54
|
+
while (!predicate()) {
|
|
55
|
+
if (Date.now() >= deadline)
|
|
56
|
+
return false;
|
|
57
|
+
await sleep(50);
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Every live seat custodian on this host, with the run that started it (#1648).
|
|
63
|
+
*
|
|
64
|
+
* This reads `/proc/<pid>/cmdline` only, which is world-readable, so a census costs one readdir plus
|
|
65
|
+
* one small read per pid and never needs the uid of the process it is looking at. That is the whole
|
|
66
|
+
* reason the marker is on argv: the orphans carried their worktree only as their CWD, and
|
|
67
|
+
* `/proc/<pid>/cwd` is a readlink the owner alone may follow, so a census of somebody else's
|
|
68
|
+
* leftovers could not even see them.
|
|
69
|
+
*
|
|
70
|
+
* `run` filters to one run's custodians. Omitted, it reports every one on the host, which is what a
|
|
71
|
+
* reaper sweeping before a run wants. A custodian started before this change carries no marker and
|
|
72
|
+
* is not reported: it cannot be attributed, and claiming it as the caller's would be a guess.
|
|
73
|
+
*/
|
|
74
|
+
export function censusCustodians(run) {
|
|
75
|
+
if (process.platform !== "linux")
|
|
76
|
+
throw unsupportedTransport();
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const entry of readdirSync("/proc")) {
|
|
79
|
+
if (!/^\d+$/.test(entry))
|
|
80
|
+
continue;
|
|
81
|
+
const pid = Number(entry);
|
|
82
|
+
let cmdline;
|
|
83
|
+
try {
|
|
84
|
+
cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf8");
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
continue; // exited between the readdir and the read, or not ours to read
|
|
88
|
+
}
|
|
89
|
+
const argv = cmdline.split("\0").filter((s) => s.length > 0);
|
|
90
|
+
// Both halves are required. The entry path alone matches any process whose argv happens to name
|
|
91
|
+
// the file; the marker alone would match this package's own tests.
|
|
92
|
+
if (!argv.some((a) => a.endsWith("/dist/custodian.js")))
|
|
93
|
+
continue;
|
|
94
|
+
const at = argv.indexOf(RUN_MARKER_FLAG);
|
|
95
|
+
if (at < 0)
|
|
96
|
+
continue;
|
|
97
|
+
const marker = argv[at + 1];
|
|
98
|
+
if (marker === undefined)
|
|
99
|
+
continue;
|
|
100
|
+
if (run !== undefined && marker !== run)
|
|
101
|
+
continue;
|
|
102
|
+
out.push({ pid, run: marker });
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Reap one custodied seat that this process did not spawn: a crashed manager's, or a seat whose
|
|
108
|
+
* custodian is lingering after its child exited. Signals only a pid whose current start identity
|
|
109
|
+
* matches the record, verifies the custodian, the child and the child's whole process group gone,
|
|
110
|
+
* then removes the custody record. A record written without start identities refuses: a bare pid
|
|
111
|
+
* cannot be told from an unrelated process that inherited it. Linux only, like the custodian.
|
|
112
|
+
*/
|
|
113
|
+
export async function reapSeat(root, id, opts = {}) {
|
|
114
|
+
if (process.platform !== "linux")
|
|
115
|
+
throw unsupportedTransport();
|
|
116
|
+
const graceMs = opts.graceMs ?? 10_000;
|
|
117
|
+
const path = recordPath(root, id);
|
|
118
|
+
let rec;
|
|
119
|
+
try {
|
|
120
|
+
rec = readRecord(path);
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
if (e.code === "ENOENT")
|
|
124
|
+
return { outcome: "absent" };
|
|
125
|
+
throw new Error(`seat ${id} record at ${path} is unreadable: ${e.message}`);
|
|
126
|
+
}
|
|
127
|
+
if (rec.custodianStart === undefined)
|
|
128
|
+
throw new Error(`seat ${id} record carries no process start identity; refusing to signal pid ${rec.custodianPid}/${rec.childPid} (a bare pid may belong to an unrelated process)`);
|
|
129
|
+
// A start token counts ticks since ITS OWN boot, so it only tells two processes apart within one
|
|
130
|
+
// boot, and this record may have outlived a reboot on disk. An unbound or foreign-boot record is
|
|
131
|
+
// refused rather than signalled: the pids in it now name whatever this boot put at those numbers.
|
|
132
|
+
const boot = bootToken();
|
|
133
|
+
if (rec.bootId === undefined)
|
|
134
|
+
throw new Error(`seat ${id} record carries no boot identity; refusing to signal pid ${rec.custodianPid}/${rec.childPid} (its start tokens cannot be compared across a reboot)`);
|
|
135
|
+
if (boot !== undefined && rec.bootId !== boot)
|
|
136
|
+
throw new Error(`seat ${id} record belongs to boot ${rec.bootId}, but this is boot ${boot}; refusing to signal pid ${rec.custodianPid}/${rec.childPid} (those pids now belong to this boot's processes)`);
|
|
137
|
+
const custodianStart = rec.custodianStart;
|
|
138
|
+
// A pinned record without a child identity means the child was gone before custody began.
|
|
139
|
+
const childLive = () => rec.childStart !== undefined && identityVerdict(rec.childPid, rec.childStart) === "live";
|
|
140
|
+
// The child first: node-pty made it a session and process-group leader, so signalling the group
|
|
141
|
+
// takes its descendants (a connector host's TUI and bridges) with it. A child that is no longer a
|
|
142
|
+
// group leader is signalled alone and its group is not claimed.
|
|
143
|
+
let child = "gone";
|
|
144
|
+
let groupKilled = false;
|
|
145
|
+
if (childLive()) {
|
|
146
|
+
const leader = processGroupOf(rec.childPid) === rec.childPid;
|
|
147
|
+
child = leader ? signal(-rec.childPid, "SIGKILL") : signal(rec.childPid, "SIGKILL");
|
|
148
|
+
groupKilled = leader && child === "signalled";
|
|
149
|
+
}
|
|
150
|
+
let custodian = "gone";
|
|
151
|
+
if (identityVerdict(rec.custodianPid, custodianStart) === "live")
|
|
152
|
+
custodian = signal(rec.custodianPid, "SIGKILL");
|
|
153
|
+
const gone = () => !childLive() && identityVerdict(rec.custodianPid, custodianStart) === "gone";
|
|
154
|
+
if (!(await until(gone, graceMs)))
|
|
155
|
+
throw new Error(`seat ${id}: custodian ${rec.custodianPid} or child ${rec.childPid} still holds its recorded start identity ${graceMs}ms after SIGKILL; exit not proved`);
|
|
156
|
+
// The group after the leader: a member that re-parented to init keeps the pgid, and the pgid
|
|
157
|
+
// cannot be reused while any member lives, so an empty group is proof for the descendants.
|
|
158
|
+
let group = 0;
|
|
159
|
+
if (groupKilled) {
|
|
160
|
+
const empty = await until(() => {
|
|
161
|
+
const members = groupMembers(rec.childPid);
|
|
162
|
+
group = members.length;
|
|
163
|
+
if (members.length)
|
|
164
|
+
for (const pid of members)
|
|
165
|
+
signal(pid, "SIGKILL");
|
|
166
|
+
return members.length === 0;
|
|
167
|
+
}, graceMs);
|
|
168
|
+
if (!empty)
|
|
169
|
+
throw new Error(`seat ${id}: ${group} process(es) still in group ${rec.childPid} ${graceMs}ms after SIGKILL; exit not proved`);
|
|
170
|
+
}
|
|
171
|
+
const dir = dirname(path);
|
|
172
|
+
if (existsSync(dir))
|
|
173
|
+
rmSync(dir, { recursive: true, force: true });
|
|
174
|
+
return {
|
|
175
|
+
outcome: "reaped",
|
|
176
|
+
custodian,
|
|
177
|
+
child,
|
|
178
|
+
group,
|
|
179
|
+
detail: `custodian ${rec.custodianPid} ${custodian}, child ${rec.childPid} ${child}${groupKilled ? `, group ${rec.childPid} empty` : ""}; custody record removed`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=reap.js.map
|
package/dist/reap.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reap.js","sourceRoot":"","sources":["../src/reap.ts"],"names":[],"mappings":"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"}
|
package/dist/record.d.ts
CHANGED
|
@@ -7,11 +7,40 @@ export interface SeatRecord {
|
|
|
7
7
|
token: string;
|
|
8
8
|
custodianPid: number;
|
|
9
9
|
childPid: number;
|
|
10
|
+
/** The custodian's process start identity (`/proc/<pid>/stat` starttime), so a later reader can
|
|
11
|
+
* tell this custodian from an unrelated process that inherited its pid. Absent on a record a
|
|
12
|
+
* custodian wrote before identity pinning; such a record is never signalled. */
|
|
13
|
+
custodianStart?: string;
|
|
14
|
+
/** The child's process start identity, same rule. Absent on a pinned record when the child had
|
|
15
|
+
* already exited before the custodian could read it (a failed exec); a reader takes that as gone. */
|
|
16
|
+
childStart?: string;
|
|
17
|
+
/** The boot this record's pids belong to (`/proc/sys/kernel/random/boot_id`). A start token counts
|
|
18
|
+
* clock ticks SINCE BOOT, so it is only unique within one boot, and custody records outlive a
|
|
19
|
+
* reboot on disk. Without this, a surviving record could match an unrelated process that reached
|
|
20
|
+
* the same pid at the same tick after a reboot, and be signalled for it. Absent on a record
|
|
21
|
+
* written before boot binding; such a record is never signalled. */
|
|
22
|
+
bootId?: string;
|
|
10
23
|
}
|
|
11
24
|
export declare function seatId(): string;
|
|
25
|
+
/** Every seat id names a directory under the custody root, so it is checked against the shape
|
|
26
|
+
* {@link seatId} mints before it is ever joined to a path. A reference reaches `reapSeat` and
|
|
27
|
+
* `loadSeat` from a durable slot row, and an id carrying `..` or a separator would address a
|
|
28
|
+
* record outside the root. Refuse rather than resolve: an unaddressable reference must not be
|
|
29
|
+
* reported as a seat that is already forgotten. */
|
|
30
|
+
export declare function assertSeatId(id: string): string;
|
|
12
31
|
export declare function capabilityToken(): string;
|
|
13
32
|
export declare function recordPath(root: string, id: string): string;
|
|
14
33
|
export declare function socketPath(root: string, id: string): string;
|
|
34
|
+
/** This boot's identity. Read at the moment a record is written and again when it is read back: a
|
|
35
|
+
* start token is ticks since THIS boot, so two records from different boots share a namespace they
|
|
36
|
+
* cannot distinguish on their own. Undefined where the kernel does not publish it, which leaves a
|
|
37
|
+
* record unbound and therefore unsignallable rather than wrongly signallable. */
|
|
38
|
+
export declare function bootToken(): string | undefined;
|
|
39
|
+
/** The Linux process start token: `/proc/<pid>/stat` field 22 (starttime, in clock ticks since
|
|
40
|
+
* boot), parsed after the final `)` because comm may hold spaces and parentheses. A zombie still
|
|
41
|
+
* reports it, so a child that exited before its custodian reaped it is still readable. Undefined
|
|
42
|
+
* when the pid has no `/proc` entry. */
|
|
43
|
+
export declare function processStartToken(pid: number): string | undefined;
|
|
15
44
|
export declare function writeRecord(path: string, record: SeatRecord): void;
|
|
16
45
|
export declare function readRecord(path: string): SeatRecord;
|
|
17
46
|
//# sourceMappingURL=record.d.ts.map
|
package/dist/record.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB;;qFAEiF;IACjF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;0GACsG;IACtG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;yEAIqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,MAAM,IAAI,MAAM,CAE/B;AAID;;;;oDAIoD;AACpD,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAG/C;AAED,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;kFAGkF;AAClF,wBAAgB,SAAS,IAAI,MAAM,GAAG,SAAS,CAO9C;AAED;;;yCAGyC;AACzC,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUjE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAKlE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CA6BnD"}
|
package/dist/record.js
CHANGED
|
@@ -5,14 +5,55 @@ export const RECORD_VERSION = 1;
|
|
|
5
5
|
export function seatId() {
|
|
6
6
|
return randomBytes(16).toString("hex");
|
|
7
7
|
}
|
|
8
|
+
const SEAT_ID = /^[0-9a-f]{32}$/;
|
|
9
|
+
/** Every seat id names a directory under the custody root, so it is checked against the shape
|
|
10
|
+
* {@link seatId} mints before it is ever joined to a path. A reference reaches `reapSeat` and
|
|
11
|
+
* `loadSeat` from a durable slot row, and an id carrying `..` or a separator would address a
|
|
12
|
+
* record outside the root. Refuse rather than resolve: an unaddressable reference must not be
|
|
13
|
+
* reported as a seat that is already forgotten. */
|
|
14
|
+
export function assertSeatId(id) {
|
|
15
|
+
if (!SEAT_ID.test(id))
|
|
16
|
+
throw new Error(`seat id ${JSON.stringify(id)} is not 32 lowercase hex characters; it names a directory under the custody root`);
|
|
17
|
+
return id;
|
|
18
|
+
}
|
|
8
19
|
export function capabilityToken() {
|
|
9
20
|
return randomBytes(32).toString("hex");
|
|
10
21
|
}
|
|
11
22
|
export function recordPath(root, id) {
|
|
12
|
-
return join(root, id, "record.json");
|
|
23
|
+
return join(root, assertSeatId(id), "record.json");
|
|
13
24
|
}
|
|
14
25
|
export function socketPath(root, id) {
|
|
15
|
-
return join(root, id, "seat.sock");
|
|
26
|
+
return join(root, assertSeatId(id), "seat.sock");
|
|
27
|
+
}
|
|
28
|
+
/** This boot's identity. Read at the moment a record is written and again when it is read back: a
|
|
29
|
+
* start token is ticks since THIS boot, so two records from different boots share a namespace they
|
|
30
|
+
* cannot distinguish on their own. Undefined where the kernel does not publish it, which leaves a
|
|
31
|
+
* record unbound and therefore unsignallable rather than wrongly signallable. */
|
|
32
|
+
export function bootToken() {
|
|
33
|
+
try {
|
|
34
|
+
const id = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
35
|
+
return id.length > 0 ? id : undefined;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** The Linux process start token: `/proc/<pid>/stat` field 22 (starttime, in clock ticks since
|
|
42
|
+
* boot), parsed after the final `)` because comm may hold spaces and parentheses. A zombie still
|
|
43
|
+
* reports it, so a child that exited before its custodian reaped it is still readable. Undefined
|
|
44
|
+
* when the pid has no `/proc` entry. */
|
|
45
|
+
export function processStartToken(pid) {
|
|
46
|
+
try {
|
|
47
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
48
|
+
const after = stat.lastIndexOf(") ");
|
|
49
|
+
if (after < 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
const token = stat.slice(after + 2).split(" ")[19];
|
|
52
|
+
return token || undefined;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
16
57
|
}
|
|
17
58
|
export function writeRecord(path, record) {
|
|
18
59
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
@@ -36,6 +77,12 @@ export function readRecord(path) {
|
|
|
36
77
|
throw new Error("seat record missing custodianPid");
|
|
37
78
|
if (typeof raw.childPid !== "number" || !Number.isInteger(raw.childPid))
|
|
38
79
|
throw new Error("seat record missing childPid");
|
|
80
|
+
if (raw.custodianStart !== undefined && (typeof raw.custodianStart !== "string" || raw.custodianStart.length === 0))
|
|
81
|
+
throw new Error("seat record custodianStart is not a start token");
|
|
82
|
+
if (raw.childStart !== undefined && (typeof raw.childStart !== "string" || raw.childStart.length === 0))
|
|
83
|
+
throw new Error("seat record childStart is not a start token");
|
|
84
|
+
if (raw.bootId !== undefined && (typeof raw.bootId !== "string" || raw.bootId.length === 0))
|
|
85
|
+
throw new Error("seat record bootId is not a boot identity");
|
|
39
86
|
return {
|
|
40
87
|
version: RECORD_VERSION,
|
|
41
88
|
id: raw.id,
|
|
@@ -44,6 +91,11 @@ export function readRecord(path) {
|
|
|
44
91
|
token: raw.token,
|
|
45
92
|
custodianPid: raw.custodianPid,
|
|
46
93
|
childPid: raw.childPid,
|
|
94
|
+
...(raw.custodianStart !== undefined ? { custodianStart: raw.custodianStart } : {}),
|
|
95
|
+
...(raw.childStart !== undefined ? { childStart: raw.childStart } : {}),
|
|
96
|
+
// Carried, not re-read from this process: a record read on another boot must still say which
|
|
97
|
+
// boot its pids came from, which is the whole point of the stamp.
|
|
98
|
+
...(raw.bootId !== undefined ? { bootId: raw.bootId } : {}),
|
|
47
99
|
};
|
|
48
100
|
}
|
|
49
101
|
//# sourceMappingURL=record.js.map
|
package/dist/record.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record.js","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC5E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"record.js","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Normalize terminal text according to LaunchSpec.confirm: ignore ANSI control sequences and whitespace. */
|
|
2
|
+
export declare function normalizeConfirmText(value: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Matches a connector-declared startup confirmation prompt across arbitrary PTY chunks.
|
|
5
|
+
* The retained text is bounded because startup TUIs can repaint indefinitely before showing a gate.
|
|
6
|
+
*/
|
|
7
|
+
export declare class StartupConfirmMatcher {
|
|
8
|
+
readonly prompt: string;
|
|
9
|
+
private readonly maxChars;
|
|
10
|
+
private readonly needle;
|
|
11
|
+
private raw;
|
|
12
|
+
private matched;
|
|
13
|
+
constructor(prompt: string, maxChars?: number);
|
|
14
|
+
push(chunk: string): boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function unmatchedConfirmMessage(prompt: string, timeoutMs: number): string;
|
|
17
|
+
//# sourceMappingURL=startup-confirm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"startup-confirm.d.ts","sourceRoot":"","sources":["../src/startup-confirm.ts"],"names":[],"mappings":"AAIA,6GAA6G;AAC7G,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;GAGG;AACH,qBAAa,qBAAqB;IAKpB,QAAQ,CAAC,MAAM,EAAE,MAAM;IAAE,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAJ9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,OAAO,CAAS;gBAEH,MAAM,EAAE,MAAM,EAAmB,QAAQ,SAAY;IAK1E,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;CAW7B;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEjF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const CSI = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
2
|
+
const OSC = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
3
|
+
const ESC = /\x1b(?:.|$)/g;
|
|
4
|
+
/** Normalize terminal text according to LaunchSpec.confirm: ignore ANSI control sequences and whitespace. */
|
|
5
|
+
export function normalizeConfirmText(value) {
|
|
6
|
+
return value.replace(OSC, "").replace(CSI, "").replace(ESC, "").replace(/\s+/g, "");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Matches a connector-declared startup confirmation prompt across arbitrary PTY chunks.
|
|
10
|
+
* The retained text is bounded because startup TUIs can repaint indefinitely before showing a gate.
|
|
11
|
+
*/
|
|
12
|
+
export class StartupConfirmMatcher {
|
|
13
|
+
prompt;
|
|
14
|
+
maxChars;
|
|
15
|
+
needle;
|
|
16
|
+
raw = "";
|
|
17
|
+
matched = false;
|
|
18
|
+
constructor(prompt, maxChars = 64 * 1024) {
|
|
19
|
+
this.prompt = prompt;
|
|
20
|
+
this.maxChars = maxChars;
|
|
21
|
+
this.needle = normalizeConfirmText(prompt);
|
|
22
|
+
if (!this.needle)
|
|
23
|
+
throw new Error("startup confirmation prompt is empty after ANSI and whitespace normalization");
|
|
24
|
+
}
|
|
25
|
+
push(chunk) {
|
|
26
|
+
if (this.matched)
|
|
27
|
+
return false;
|
|
28
|
+
this.raw += chunk;
|
|
29
|
+
const text = normalizeConfirmText(this.raw);
|
|
30
|
+
if (text.includes(this.needle)) {
|
|
31
|
+
this.matched = true;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (this.raw.length > this.maxChars)
|
|
35
|
+
this.raw = this.raw.slice(-this.maxChars);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function unmatchedConfirmMessage(prompt, timeoutMs) {
|
|
40
|
+
return `Cotal startup confirmation failed: prompt ${JSON.stringify(prompt)} did not appear within ${timeoutMs}ms.`;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=startup-confirm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"startup-confirm.js","sourceRoot":"","sources":["../src/startup-confirm.ts"],"names":[],"mappings":"AAAA,MAAM,GAAG,GAAG,0BAA0B,CAAC;AACvC,MAAM,GAAG,GAAG,oCAAoC,CAAC;AACjD,MAAM,GAAG,GAAG,cAAc,CAAC;AAE3B,6GAA6G;AAC7G,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,OAAO,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACtF,CAAC;AAED;;;GAGG;AACH,MAAM,OAAO,qBAAqB;IAKX;IAAiC;IAJrC,MAAM,CAAS;IACxB,GAAG,GAAG,EAAE,CAAC;IACT,OAAO,GAAG,KAAK,CAAC;IAExB,YAAqB,MAAc,EAAmB,WAAW,EAAE,GAAG,IAAI;QAArD,WAAM,GAAN,MAAM,CAAQ;QAAmB,aAAQ,GAAR,QAAQ,CAAY;QACxE,IAAI,CAAC,MAAM,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;QAC3C,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,8EAA8E,CAAC,CAAC;IACpH,CAAC;IAED,IAAI,CAAC,KAAa;QAChB,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC;QAC/B,IAAI,CAAC,GAAG,IAAI,KAAK,CAAC;QAClB,MAAM,IAAI,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC5C,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC/B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,QAAQ;YAAE,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC/E,OAAO,KAAK,CAAC;IACf,CAAC;CACF;AAED,MAAM,UAAU,uBAAuB,CAAC,MAAc,EAAE,SAAiB;IACvE,OAAO,6CAA6C,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,0BAA0B,SAAS,KAAK,CAAC;AACrH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cotal-ai/seat",
|
|
3
3
|
"description": "Local PTY seat custody: one detached custodian process per seat, a one-shot launcher, and the authenticated local protocol a manager worker uses to adopt the handle. Depends on nothing else in the repo.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.50.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -29,9 +29,13 @@
|
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@cotal-ai/smoke-kit": "0.0.0"
|
|
34
|
+
},
|
|
32
35
|
"scripts": {
|
|
33
36
|
"typecheck": "tsc -p tsconfig.check.json",
|
|
34
37
|
"build": "node scripts/build-native.mjs && tsc -p tsconfig.json",
|
|
38
|
+
"build:ts": "tsc -p tsconfig.json",
|
|
35
39
|
"build:native": "node scripts/build-native.mjs",
|
|
36
40
|
"test": "pnpm run build && tsx smoke/platform.smoke.ts && tsx smoke/protocol.smoke.ts && tsx smoke/lifecycle.smoke.ts && tsx smoke/isolation.smoke.ts"
|
|
37
41
|
}
|