@descryy/runtime-controller 0.3.8 → 0.4.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.
Files changed (77) hide show
  1. package/dist/attach-discovery.d.ts +94 -0
  2. package/dist/attach-discovery.d.ts.map +1 -0
  3. package/dist/attach-discovery.js +238 -0
  4. package/dist/attach-discovery.js.map +1 -0
  5. package/dist/attach-fencing.d.ts +109 -0
  6. package/dist/attach-fencing.d.ts.map +1 -0
  7. package/dist/attach-fencing.js +215 -0
  8. package/dist/attach-fencing.js.map +1 -0
  9. package/dist/capability-registry.d.ts +9 -0
  10. package/dist/capability-registry.d.ts.map +1 -0
  11. package/dist/capability-registry.js +32 -0
  12. package/dist/capability-registry.js.map +1 -0
  13. package/dist/collector-version.d.ts +3 -0
  14. package/dist/collector-version.d.ts.map +1 -0
  15. package/dist/collector-version.js +5 -0
  16. package/dist/collector-version.js.map +1 -0
  17. package/dist/container-sandbox.d.ts +104 -0
  18. package/dist/container-sandbox.d.ts.map +1 -0
  19. package/dist/container-sandbox.js +152 -0
  20. package/dist/container-sandbox.js.map +1 -0
  21. package/dist/controller.d.ts +152 -0
  22. package/dist/controller.d.ts.map +1 -0
  23. package/dist/controller.js +464 -0
  24. package/dist/controller.js.map +1 -0
  25. package/dist/dependency-version-check.d.ts +36 -0
  26. package/dist/dependency-version-check.d.ts.map +1 -0
  27. package/dist/dependency-version-check.js +72 -0
  28. package/dist/dependency-version-check.js.map +1 -0
  29. package/dist/env.d.ts +10 -0
  30. package/dist/env.d.ts.map +1 -0
  31. package/dist/env.js +12 -0
  32. package/dist/env.js.map +1 -0
  33. package/dist/environment-metadata.d.ts +15 -0
  34. package/dist/environment-metadata.d.ts.map +1 -0
  35. package/dist/environment-metadata.js +63 -0
  36. package/dist/environment-metadata.js.map +1 -0
  37. package/dist/environment-version-check.d.ts +40 -0
  38. package/dist/environment-version-check.d.ts.map +1 -0
  39. package/dist/environment-version-check.js +176 -0
  40. package/dist/environment-version-check.js.map +1 -0
  41. package/dist/execution-safety.d.ts +96 -0
  42. package/dist/execution-safety.d.ts.map +1 -0
  43. package/dist/execution-safety.js +140 -0
  44. package/dist/execution-safety.js.map +1 -0
  45. package/dist/index.d.ts +33 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +18 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/orchestration.d.ts +55 -0
  50. package/dist/orchestration.d.ts.map +1 -0
  51. package/dist/orchestration.js +219 -0
  52. package/dist/orchestration.js.map +1 -0
  53. package/dist/preflight.d.ts +122 -0
  54. package/dist/preflight.d.ts.map +1 -0
  55. package/dist/preflight.js +177 -0
  56. package/dist/preflight.js.map +1 -0
  57. package/dist/process-collector.d.ts +20 -0
  58. package/dist/process-collector.d.ts.map +1 -0
  59. package/dist/process-collector.js +116 -0
  60. package/dist/process-collector.js.map +1 -0
  61. package/dist/process-identity.d.ts +46 -0
  62. package/dist/process-identity.d.ts.map +1 -0
  63. package/dist/process-identity.js +186 -0
  64. package/dist/process-identity.js.map +1 -0
  65. package/dist/process-manager.d.ts +137 -0
  66. package/dist/process-manager.d.ts.map +1 -0
  67. package/dist/process-manager.js +382 -0
  68. package/dist/process-manager.js.map +1 -0
  69. package/dist/readiness.d.ts +122 -0
  70. package/dist/readiness.d.ts.map +1 -0
  71. package/dist/readiness.js +214 -0
  72. package/dist/readiness.js.map +1 -0
  73. package/dist/sandbox.d.ts +94 -0
  74. package/dist/sandbox.d.ts.map +1 -0
  75. package/dist/sandbox.js +228 -0
  76. package/dist/sandbox.js.map +1 -0
  77. package/package.json +2 -2
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Attach-mode discovery (H-attach-log-discovery): answers "what pid, if any,
3
+ * is listening on this port" and "where does that pid's own output go"
4
+ * from process/kernel state alone -- never by inferring what a framework
5
+ * logs, only by reading what the OS already knows. This is what lets
6
+ * `observe`'s attach mode work without the caller supplying a pid or a log
7
+ * file (`descry-core`'s own observe tool composes these two, once its pin on
8
+ * this package moves past the version that added them).
9
+ *
10
+ * Two independent lookups:
11
+ * - `findPortOwner` (port -> pid): reuses the same `/proc/net/tcp{,6}` +
12
+ * per-pid fd-table matching `process-identity.ts`'s `verifyListeningSocketOwner`
13
+ * already does for a DIFFERENT question (does a KNOWN pid own a port) --
14
+ * this asks the converse (which pid, if any, owns it), so a second, small,
15
+ * pure copy of the inode-matching core lives here rather than importing
16
+ * that module's private helpers across a real seam boundary (this file's
17
+ * own `reader` injection point, `process-identity.ts` has none).
18
+ * - `resolveLogSourceForPid` (pid -> log source descriptor): `readlink`s the
19
+ * pid's own fd 1 and fd 2. A regular file is the file-attach path this
20
+ * repo already tails with fencing (`attachManagedProcess`/
21
+ * `attachToRunningProcess`) -- returned as a path for the caller to feed
22
+ * into that machinery, not re-implemented here. A pipe or socket (a shell
23
+ * redirect, a container runtime, a terminal) can't be read by another
24
+ * process directly, so this falls back to the pid's own cgroup membership:
25
+ * a Docker container's cgroup path names it, a systemd unit's cgroup path
26
+ * names it. Building the actual stream source for those two
27
+ * (`docker logs -f` / `journalctl -f`) is `@descryy/runtime-orchestrator`'s
28
+ * job (`attach-log-sources.ts`) -- this package doesn't depend on the
29
+ * `ProcessOutputSource` contract at all, and doesn't need to.
30
+ *
31
+ * **Injectable `ProcReader`, not real `/proc`, in tests.** Real Docker/systemd
32
+ * cgroup membership can't be fabricated hermetically against the real
33
+ * filesystem, and even the plain-file/plain-port cases are easier to prove
34
+ * correct against a fake than a real, racy `/proc`. Both functions default to
35
+ * `REAL_PROC_READER` and take an injected one for tests.
36
+ */
37
+ /** The subset of filesystem reads this module needs from `/proc` -- injectable so tests never touch the real one. */
38
+ export interface ProcReader {
39
+ readFile(path: string): string;
40
+ readdir(path: string): string[];
41
+ readlink(path: string): string;
42
+ /** `"dev:ino"` when `path` (symlinks followed) is a regular file, else `null` -- never a throw. */
43
+ regularFileIdentity(path: string): string | null;
44
+ }
45
+ export declare const REAL_PROC_READER: ProcReader;
46
+ export type PortOwnerLookup = {
47
+ readonly outcome: "found";
48
+ readonly pid: number;
49
+ } | {
50
+ readonly outcome: "nothing-listening";
51
+ } | {
52
+ readonly outcome: "several-owners";
53
+ readonly pids: readonly number[];
54
+ } | {
55
+ readonly outcome: "owned-by-docker-proxy";
56
+ readonly pid: number;
57
+ } | {
58
+ readonly outcome: "proc-unreadable";
59
+ readonly reason: string;
60
+ };
61
+ /**
62
+ * Who, if anyone, is listening on `port` right now -- a real kernel-state
63
+ * lookup (`/proc/net/tcp{,6}` plus each candidate pid's own fd table), not a
64
+ * guess. `owned-by-docker-proxy` is named separately from `found` rather than
65
+ * silently returned as one: a published Docker port is commonly fronted by a
66
+ * host-side `docker-proxy` process, not the container's own pid, and treating
67
+ * that pid as the target would attach to the wrong process entirely.
68
+ */
69
+ export declare function findPortOwner(port: number, reader?: ProcReader, platform?: NodeJS.Platform): PortOwnerLookup;
70
+ export type LogSourceDescriptor = {
71
+ readonly kind: "file";
72
+ readonly path: string;
73
+ } | {
74
+ readonly kind: "docker";
75
+ readonly container: string;
76
+ } | {
77
+ readonly kind: "systemd";
78
+ readonly unit: string;
79
+ } | {
80
+ readonly kind: "none";
81
+ readonly reason: string;
82
+ };
83
+ /**
84
+ * Where `pid`'s own log output can be read from, without the caller naming a
85
+ * pid or a log file. `file` reuses this repo's existing fenced file-tail path
86
+ * (`attachManagedProcess`/`attachToRunningProcess`) -- returned as a path for
87
+ * the caller to feed into that machinery, not re-implemented here.
88
+ * `docker`/`systemd` name a stream-only source `@descryy/runtime-orchestrator`
89
+ * builds directly -- neither has pre-existing content to fence, since both
90
+ * start reading from "now" (see `DiscoveredAttachLogSource`'s own doc,
91
+ * `@descryy/runtime-contracts`).
92
+ */
93
+ export declare function resolveLogSourceForPid(pid: number, reader?: ProcReader, platform?: NodeJS.Platform): LogSourceDescriptor;
94
+ //# sourceMappingURL=attach-discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach-discovery.d.ts","sourceRoot":"","sources":["../src/attach-discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAIH,qHAAqH;AACrH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,mGAAmG;IACnG,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAClD;AAED,eAAO,MAAM,gBAAgB,EAAE,UAY9B,CAAC;AA6DF,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAA;CAAE,GACzC;IAAE,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACxE;IAAE,QAAQ,CAAC,OAAO,EAAE,uBAAuB,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACnE;IAAE,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAErE;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,UAA6B,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,eAAe,CA0BhJ;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAiDvD;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,UAA6B,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,mBAAmB,CA4B5J"}
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Attach-mode discovery (H-attach-log-discovery): answers "what pid, if any,
3
+ * is listening on this port" and "where does that pid's own output go"
4
+ * from process/kernel state alone -- never by inferring what a framework
5
+ * logs, only by reading what the OS already knows. This is what lets
6
+ * `observe`'s attach mode work without the caller supplying a pid or a log
7
+ * file (`descry-core`'s own observe tool composes these two, once its pin on
8
+ * this package moves past the version that added them).
9
+ *
10
+ * Two independent lookups:
11
+ * - `findPortOwner` (port -> pid): reuses the same `/proc/net/tcp{,6}` +
12
+ * per-pid fd-table matching `process-identity.ts`'s `verifyListeningSocketOwner`
13
+ * already does for a DIFFERENT question (does a KNOWN pid own a port) --
14
+ * this asks the converse (which pid, if any, owns it), so a second, small,
15
+ * pure copy of the inode-matching core lives here rather than importing
16
+ * that module's private helpers across a real seam boundary (this file's
17
+ * own `reader` injection point, `process-identity.ts` has none).
18
+ * - `resolveLogSourceForPid` (pid -> log source descriptor): `readlink`s the
19
+ * pid's own fd 1 and fd 2. A regular file is the file-attach path this
20
+ * repo already tails with fencing (`attachManagedProcess`/
21
+ * `attachToRunningProcess`) -- returned as a path for the caller to feed
22
+ * into that machinery, not re-implemented here. A pipe or socket (a shell
23
+ * redirect, a container runtime, a terminal) can't be read by another
24
+ * process directly, so this falls back to the pid's own cgroup membership:
25
+ * a Docker container's cgroup path names it, a systemd unit's cgroup path
26
+ * names it. Building the actual stream source for those two
27
+ * (`docker logs -f` / `journalctl -f`) is `@descryy/runtime-orchestrator`'s
28
+ * job (`attach-log-sources.ts`) -- this package doesn't depend on the
29
+ * `ProcessOutputSource` contract at all, and doesn't need to.
30
+ *
31
+ * **Injectable `ProcReader`, not real `/proc`, in tests.** Real Docker/systemd
32
+ * cgroup membership can't be fabricated hermetically against the real
33
+ * filesystem, and even the plain-file/plain-port cases are easier to prove
34
+ * correct against a fake than a real, racy `/proc`. Both functions default to
35
+ * `REAL_PROC_READER` and take an injected one for tests.
36
+ */
37
+ import { readFileSync, readdirSync, readlinkSync, statSync } from "node:fs";
38
+ export const REAL_PROC_READER = {
39
+ readFile: (path) => readFileSync(path, "utf8"),
40
+ readdir: (path) => readdirSync(path),
41
+ readlink: (path) => readlinkSync(path),
42
+ regularFileIdentity: (path) => {
43
+ try {
44
+ const stat = statSync(path);
45
+ return stat.isFile() ? `${String(stat.dev)}:${String(stat.ino)}` : null;
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ },
51
+ };
52
+ function hexPort(port) {
53
+ return port.toString(16).toUpperCase().padStart(4, "0");
54
+ }
55
+ /** Every inode `/proc/net/tcp{,6}` records as LISTENing on `port`. A small, deliberate second copy of `process-identity.ts`'s private `listenInodesForPort` -- see this file's own header for why it isn't imported. */
56
+ function listenInodesForPort(reader, port) {
57
+ const inodes = new Set();
58
+ const targetPort = hexPort(port);
59
+ const TCP_LISTEN_STATE = "0A";
60
+ for (const path of ["/proc/net/tcp", "/proc/net/tcp6"]) {
61
+ let content;
62
+ try {
63
+ content = reader.readFile(path);
64
+ }
65
+ catch {
66
+ continue;
67
+ }
68
+ for (const line of content.split("\n").slice(1)) {
69
+ const fields = line.trim().split(/\s+/);
70
+ const localAddress = fields[1];
71
+ const state = fields[3];
72
+ const inode = fields[9];
73
+ if (localAddress === undefined || state === undefined || inode === undefined)
74
+ continue;
75
+ if (state !== TCP_LISTEN_STATE)
76
+ continue;
77
+ if (localAddress.split(":")[1] === targetPort)
78
+ inodes.add(inode);
79
+ }
80
+ }
81
+ return inodes;
82
+ }
83
+ /** Whether `pid`'s fd table holds one of `inodes`. `false`, never a throw, for a pid whose fd table cannot be read (exited mid-scan, or a permission boundary) -- the same benign-race tolerance this repo's other `/proc` scanners apply. */
84
+ function ownsOneOf(reader, pid, inodes) {
85
+ let fds;
86
+ try {
87
+ fds = reader.readdir(`/proc/${pid}/fd`);
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ const SOCKET_FD_PATTERN = /^socket:\[(\d+)\]$/;
93
+ for (const fd of fds) {
94
+ let link;
95
+ try {
96
+ link = reader.readlink(`/proc/${pid}/fd/${fd}`);
97
+ }
98
+ catch {
99
+ continue;
100
+ }
101
+ const match = SOCKET_FD_PATTERN.exec(link);
102
+ if (match !== null && inodes.has(match[1]))
103
+ return true;
104
+ }
105
+ return false;
106
+ }
107
+ function commOf(reader, pid) {
108
+ try {
109
+ return reader.readFile(`/proc/${pid}/comm`).trim();
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
115
+ /**
116
+ * Who, if anyone, is listening on `port` right now -- a real kernel-state
117
+ * lookup (`/proc/net/tcp{,6}` plus each candidate pid's own fd table), not a
118
+ * guess. `owned-by-docker-proxy` is named separately from `found` rather than
119
+ * silently returned as one: a published Docker port is commonly fronted by a
120
+ * host-side `docker-proxy` process, not the container's own pid, and treating
121
+ * that pid as the target would attach to the wrong process entirely.
122
+ */
123
+ export function findPortOwner(port, reader = REAL_PROC_READER, platform = process.platform) {
124
+ if (platform !== "linux") {
125
+ return { outcome: "proc-unreadable", reason: `port ownership lookup requires /proc, which is Linux-only; not available on "${platform}"` };
126
+ }
127
+ const inodes = listenInodesForPort(reader, port);
128
+ if (inodes.size === 0)
129
+ return { outcome: "nothing-listening" };
130
+ let pidEntries;
131
+ try {
132
+ pidEntries = reader.readdir("/proc").filter((entry) => /^\d+$/.test(entry));
133
+ }
134
+ catch (error) {
135
+ return {
136
+ outcome: "proc-unreadable",
137
+ reason: `could not list /proc to find the process listening on port ${String(port)}: ${error instanceof Error ? error.message : String(error)}`,
138
+ };
139
+ }
140
+ const owners = pidEntries.filter((pid) => ownsOneOf(reader, pid, inodes)).map(Number);
141
+ if (owners.length === 0)
142
+ return { outcome: "nothing-listening" };
143
+ if (owners.length > 1)
144
+ return { outcome: "several-owners", pids: owners };
145
+ const pid = owners[0];
146
+ if (commOf(reader, String(pid)) === "docker-proxy") {
147
+ return { outcome: "owned-by-docker-proxy", pid };
148
+ }
149
+ return { outcome: "found", pid };
150
+ }
151
+ /**
152
+ * The path of the regular file `pid` holds open on `fd`, or `null`. An absolute link target
153
+ * is not enough: a terminal (`/dev/pts/3`) and `/dev/null` are absolute too, and tailing a
154
+ * terminal device would read keystrokes, not logs. So the open file itself (`/proc/<pid>/fd/N`,
155
+ * which stat follows to the real inode) must be a regular file, and the path must still name
156
+ * that same inode -- after a log rotation the process keeps writing the old file while the path
157
+ * names a new one, and tailing the path would read a file nobody is writing to.
158
+ */
159
+ function fileTargetOf(reader, pid, fd) {
160
+ const fdPath = `/proc/${String(pid)}/fd/${String(fd)}`;
161
+ let link;
162
+ try {
163
+ link = reader.readlink(fdPath);
164
+ }
165
+ catch {
166
+ return null;
167
+ }
168
+ if (!link.startsWith("/"))
169
+ return null;
170
+ const held = reader.regularFileIdentity(fdPath);
171
+ if (held === null)
172
+ return null;
173
+ return reader.regularFileIdentity(link) === held ? link : null;
174
+ }
175
+ /** `/proc/<pid>/cgroup`'s docker container id, from either the legacy `.../docker/<id>` path or the cgroup-v2 `.../docker-<id>.scope` form. `null` when neither pattern is present. */
176
+ function dockerContainerFromCgroup(cgroup) {
177
+ const legacy = /\/docker\/([0-9a-f]{12,64})/.exec(cgroup);
178
+ if (legacy?.[1] !== undefined)
179
+ return legacy[1];
180
+ const scoped = /docker-([0-9a-f]{12,64})\.scope/.exec(cgroup);
181
+ return scoped?.[1] ?? null;
182
+ }
183
+ /**
184
+ * The system unit `pid` runs as, e.g. `myapp.service` from `0::/system.slice/myapp.service`.
185
+ * Only when that unit is the process's own cgroup leaf under `system.slice`: an app started
186
+ * from a desktop terminal sits under `user@1000.service/...`, the per-user manager, whose
187
+ * journal is every desktop app's output, and a user service's journal is not what
188
+ * `journalctl -u` reads. Either would attribute someone else's logs to the target, so
189
+ * both are `null` here and disclosed as unreadable rather than guessed.
190
+ */
191
+ function systemdUnitFromCgroup(cgroup) {
192
+ for (const line of cgroup.split("\n")) {
193
+ const path = line.slice(line.indexOf(":", line.indexOf(":") + 1) + 1);
194
+ const match = /^\/system\.slice\/(?:[\w.@-]+\.slice\/)*([\w.@-]+\.service)$/.exec(path);
195
+ if (match?.[1] !== undefined)
196
+ return match[1];
197
+ }
198
+ return null;
199
+ }
200
+ /**
201
+ * Where `pid`'s own log output can be read from, without the caller naming a
202
+ * pid or a log file. `file` reuses this repo's existing fenced file-tail path
203
+ * (`attachManagedProcess`/`attachToRunningProcess`) -- returned as a path for
204
+ * the caller to feed into that machinery, not re-implemented here.
205
+ * `docker`/`systemd` name a stream-only source `@descryy/runtime-orchestrator`
206
+ * builds directly -- neither has pre-existing content to fence, since both
207
+ * start reading from "now" (see `DiscoveredAttachLogSource`'s own doc,
208
+ * `@descryy/runtime-contracts`).
209
+ */
210
+ export function resolveLogSourceForPid(pid, reader = REAL_PROC_READER, platform = process.platform) {
211
+ if (platform !== "linux") {
212
+ return { kind: "none", reason: `log-source discovery reads /proc, which is Linux-only; not available on "${platform}"` };
213
+ }
214
+ const filePath = fileTargetOf(reader, pid, 1) ?? fileTargetOf(reader, pid, 2);
215
+ if (filePath !== null)
216
+ return { kind: "file", path: filePath };
217
+ let cgroup;
218
+ try {
219
+ cgroup = reader.readFile(`/proc/${String(pid)}/cgroup`);
220
+ }
221
+ catch (error) {
222
+ return {
223
+ kind: "none",
224
+ reason: `pid ${String(pid)}'s stdout/stderr is neither a regular file nor a terminal/pipe this process can name a container or service for, and its cgroup membership could not be read: ${error instanceof Error ? error.message : String(error)}`,
225
+ };
226
+ }
227
+ const container = dockerContainerFromCgroup(cgroup);
228
+ if (container !== null)
229
+ return { kind: "docker", container };
230
+ const unit = systemdUnitFromCgroup(cgroup);
231
+ if (unit !== null)
232
+ return { kind: "systemd", unit };
233
+ return {
234
+ kind: "none",
235
+ reason: `pid ${String(pid)}'s stdout/stderr is a pipe or terminal, not a regular file -- another process cannot read it, and its cgroup names neither a Docker container nor a systemd unit to follow instead.`,
236
+ };
237
+ }
238
+ //# sourceMappingURL=attach-discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach-discovery.js","sourceRoot":"","sources":["../src/attach-discovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Attach fencing (RT-N3): the boundary between "content this run watched happen"
3
+ * and "content that already existed when this run started watching it."
4
+ *
5
+ * Both `orchestrator/src/attach-to-running-process.ts` and this package's own
6
+ * `attachManagedProcess` read a target's ENTIRE log file on every poll —
7
+ * necessary, since a plain file offers no delta API — which means the very
8
+ * first read hands back everything the target had already written before
9
+ * attach, indistinguishable from something that happens during this run
10
+ * unless something records the boundary.
11
+ *
12
+ * Real incident (`documents/plans/descry-uat-phase-4.md`, finding N3): a
13
+ * killed, orphaned FIRST process took its full timeout to exit and kept
14
+ * writing to the SAME log path a SECOND, successful process also used;
15
+ * attaching to the second process's pid read the first process's crash as if
16
+ * it were the second's own. Two independent fences close two independent
17
+ * parts of that gap:
18
+ *
19
+ * - `computeAttachFence` records, at the moment of attach, how much of the
20
+ * log file already existed (`attachOffsetBytes`, `preExistingLineCount`)
21
+ * and the target's own process start time (best-effort, Linux only) — the
22
+ * information a caller needs to mark already-written content instead of
23
+ * treating it as freshly observed.
24
+ * - `detectOtherLogWriters` answers a different question: is some OTHER
25
+ * process, right now, also holding this same log file open for writing?
26
+ * On Linux only, by scanning `/proc/*\/fd`. A genuinely shared log (e.g. a
27
+ * `docker compose` stack's combined stream) is a legitimate answer here,
28
+ * not refused — this function reports who else has the file open, and it
29
+ * is the caller's job to decide what that means for a given target.
30
+ *
31
+ * Neither function makes attach perfectly correct — a log file carries no
32
+ * per-line writer-pid metadata, so "verify every line against the declared
33
+ * pid" is not implementable over a plain text file. What these two give a
34
+ * caller together: the boundary of what this run actually watched happen,
35
+ * and a best-effort, honestly-labelled answer to "is anything else writing
36
+ * here right now."
37
+ */
38
+ export type ProcessStartTimeResult = {
39
+ readonly ok: true;
40
+ readonly startTimeMs: number;
41
+ } | {
42
+ readonly ok: false;
43
+ readonly reason: string;
44
+ };
45
+ /**
46
+ * `pid`'s own start time, as wall-clock epoch milliseconds — Linux only.
47
+ * `/proc/<pid>/stat` field 22 (`starttime`) is clock ticks since boot, not
48
+ * since epoch; converted via `/proc/uptime`'s boot-relative uptime, itself
49
+ * converted to an epoch instant from `Date.now()` at read time (so this is
50
+ * only as precise as the gap between the two reads, sub-millisecond in
51
+ * practice).
52
+ *
53
+ * The `comm` field (`/proc/pid/stat` field 2) can itself contain spaces and
54
+ * parentheses — parsed by finding the LAST `)` on the line, matching every
55
+ * real `/proc/pid/stat` parser: the kernel guarantees the true comm field
56
+ * ends at the last `)`, whatever it contains.
57
+ */
58
+ export declare function getProcessStartTimeMs(pid: number, platform?: NodeJS.Platform): ProcessStartTimeResult;
59
+ export type OtherLogWriterCheck = {
60
+ readonly checked: true;
61
+ readonly otherWriterPids: readonly number[];
62
+ } | {
63
+ readonly checked: false;
64
+ readonly reason: string;
65
+ };
66
+ /**
67
+ * Every OTHER process (not `targetPid`) currently holding an open file
68
+ * descriptor to `logFilePath`, by scanning `/proc/*\/fd` and comparing each
69
+ * descriptor's real target against `logFilePath`'s own real path. Linux only.
70
+ *
71
+ * A non-empty result is not by itself a problem — a deliberately shared log
72
+ * (several processes in one `docker compose` stack writing one combined
73
+ * stream) is legitimate multi-writer use. This function only answers "who
74
+ * else has it open right now"; a caller decides what that means for a given
75
+ * target.
76
+ *
77
+ * `checked: false` (never a thrown exception) only for what stops the scan
78
+ * from running AT ALL: non-Linux, `/proc` itself unreadable, or the log path
79
+ * can't be resolved to a real path. A single pid's `/proc/<pid>/fd` being
80
+ * unreadable (exited mid-scan, or a permission boundary) is NOT that —
81
+ * skipped for that one pid only, the same benign-race tolerance
82
+ * `inferRespawnCommandFromProc` already applies to `/proc` reads elsewhere
83
+ * in this codebase.
84
+ */
85
+ export declare function detectOtherLogWriters(logFilePath: string, targetPid: number, platform?: NodeJS.Platform): OtherLogWriterCheck;
86
+ export interface AttachFence {
87
+ /** The log file's size in bytes at the moment of attach — rotation detection compares this (or a later high-water mark) against the file's current size. */
88
+ readonly attachOffsetBytes: number;
89
+ /**
90
+ * How many complete lines already existed in the log file at the moment of
91
+ * attach. A source that yields lines in file order marks the first
92
+ * this-many `preExisting: true`. See `countCompleteLines` for the one
93
+ * disclosed edge case (a line torn exactly at the attach instant).
94
+ */
95
+ readonly preExistingLineCount: number;
96
+ /** Best-effort, Linux-only; `ok: false` on any other platform or read failure — never blocks the attach itself. */
97
+ readonly processStartTime: ProcessStartTimeResult;
98
+ /** See `detectOtherLogWriters` — computed once, at the moment of attach, against the pid this fence was built for. */
99
+ readonly otherWriters: OtherLogWriterCheck;
100
+ readonly attachedAt: string;
101
+ }
102
+ /**
103
+ * Snapshots the fence at the moment of attach. Never throws: a log file that
104
+ * doesn't exist yet at attach time (a target that hasn't written anything) is
105
+ * an honest zero-backlog fence, not an error — the file-tail machinery
106
+ * downstream already tolerates a not-yet-existing path the same way.
107
+ */
108
+ export declare function computeAttachFence(pid: number, logFilePath: string, platform?: NodeJS.Platform): AttachFence;
109
+ //# sourceMappingURL=attach-fencing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach-fencing.d.ts","sourceRoot":"","sources":["../src/attach-fencing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAsBH,MAAM,MAAM,sBAAsB,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAwB3I;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,sBAAsB,CAiCvH;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACvE;IAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,mBAAmB,CA+C/I;AAED,MAAM,WAAW,WAAW;IAC1B,4JAA4J;IAC5J,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,mHAAmH;IACnH,QAAQ,CAAC,gBAAgB,EAAE,sBAAsB,CAAC;IAClD,sHAAsH;IACtH,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,WAAW,CAgB9H"}