@primafuture/systemd-tasks 1.0.0 → 1.1.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/CHANGELOG.md +27 -0
- package/README.md +86 -8
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/guardian.mjs +212 -0
- package/dist/guardian.mjs.map +1 -0
- package/dist/index.cjs +9 -13287
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -316
- package/dist/index.d.ts +1 -316
- package/dist/index.mjs +2 -13304
- package/dist/index.mjs.map +1 -1
- package/dist/observed.cjs +35 -0
- package/dist/observed.cjs.map +1 -0
- package/dist/observed.d.cts +1 -0
- package/dist/observed.d.ts +1 -0
- package/dist/observed.mjs +8 -0
- package/dist/observed.mjs.map +1 -0
- package/dist/runtime.cjs +14229 -0
- package/dist/runtime.cjs.map +1 -0
- package/dist/runtime.d.cts +467 -0
- package/docs/implementation-decisions.md +137 -0
- package/package.json +27 -7
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import * as util from 'node:util';
|
|
2
|
+
|
|
3
|
+
/** A deliberately small allowlist of properties that cannot replace task lifecycle policy. */
|
|
4
|
+
interface SystemdProperty {
|
|
5
|
+
/** The resource weight to pass to the transient service. */
|
|
6
|
+
readonly name: 'CPUWeight' | 'IOWeight';
|
|
7
|
+
/** The systemd D-Bus signature, independent of the transport implementation. */
|
|
8
|
+
readonly signature: 't';
|
|
9
|
+
/** An integer resource weight from 1 through 10000. */
|
|
10
|
+
readonly value: number;
|
|
11
|
+
}
|
|
12
|
+
/** A command specification suitable for serialization before any attempt to execute it. */
|
|
13
|
+
interface TaskSpec {
|
|
14
|
+
/** Absolute executable path; no PATH lookup or shell is performed. */
|
|
15
|
+
readonly executable: string;
|
|
16
|
+
/** Literal arguments excluding argv[0], which is the executable path. */
|
|
17
|
+
readonly argv: readonly string[];
|
|
18
|
+
/** Absolute working directory, which must already exist at execution time. */
|
|
19
|
+
readonly cwd: string;
|
|
20
|
+
/** Explicit environment entries; systemd may add its own execution metadata. */
|
|
21
|
+
readonly env: Readonly<Record<string, string>>;
|
|
22
|
+
/** Positive integer milliseconds, or null to explicitly disable the runtime limit. */
|
|
23
|
+
readonly runtimeLimitMs: number | null;
|
|
24
|
+
/** Positive integer milliseconds before manager-owned SIGKILL escalation. */
|
|
25
|
+
readonly stopGraceMs: number;
|
|
26
|
+
/** Optional resource weights; all other low-level properties are reserved. */
|
|
27
|
+
readonly systemdProperties?: readonly SystemdProperty[];
|
|
28
|
+
}
|
|
29
|
+
/** Common serializable ownership evidence, without environment or command arguments. */
|
|
30
|
+
interface TaskIdentity {
|
|
31
|
+
/** Serialization version understood by this library. */
|
|
32
|
+
readonly version: 1;
|
|
33
|
+
/** Random UUID generated before submission. */
|
|
34
|
+
readonly id: string;
|
|
35
|
+
/** Unique transient service name derived from the ID. */
|
|
36
|
+
readonly unitName: string;
|
|
37
|
+
/** Unique auxiliary service name derived from the same ID. */
|
|
38
|
+
readonly retentionUnitName: string;
|
|
39
|
+
/** V1 supports the local user manager only. */
|
|
40
|
+
readonly managerScope: 'user';
|
|
41
|
+
/** SHA-256 fingerprint of the normalized task specification. */
|
|
42
|
+
readonly fingerprint: string;
|
|
43
|
+
}
|
|
44
|
+
/** A prepared intention; saving it does not imply that execution was attempted. */
|
|
45
|
+
interface TaskIntent extends TaskIdentity {
|
|
46
|
+
/** Absence of the observed discriminator keeps both serialized intent families disjoint. */
|
|
47
|
+
readonly kind?: never;
|
|
48
|
+
/** Execution configuration, potentially containing secrets; store accordingly. */
|
|
49
|
+
readonly spec: TaskSpec;
|
|
50
|
+
}
|
|
51
|
+
/** Identity of the specific environment in which a task was observed. */
|
|
52
|
+
interface ManagerIdentity {
|
|
53
|
+
/** Manager privilege scope; system-manager references are not supported in V1. */
|
|
54
|
+
readonly scope: 'user';
|
|
55
|
+
/** Effective local UID whose user bus owns the task. */
|
|
56
|
+
readonly uid: number;
|
|
57
|
+
/** Machine identity reported by the manager's D-Bus peer. */
|
|
58
|
+
readonly machineId: string;
|
|
59
|
+
/** Local kernel boot identity, preventing cross-boot adoption. */
|
|
60
|
+
readonly bootId: string;
|
|
61
|
+
/** Unique identity of the user message bus. */
|
|
62
|
+
readonly busId: string;
|
|
63
|
+
/** Unique bus owner of org.freedesktop.systemd1 at connection time. */
|
|
64
|
+
readonly owner: string;
|
|
65
|
+
}
|
|
66
|
+
/** An observed pair of unit instances; null IDs explicitly mean incomplete evidence. */
|
|
67
|
+
interface TaskReference extends TaskIdentity {
|
|
68
|
+
/** Absence of the observed discriminator prevents cross-topology client calls. */
|
|
69
|
+
readonly kind?: never;
|
|
70
|
+
/** The manager in which the reference was obtained. */
|
|
71
|
+
readonly manager: ManagerIdentity;
|
|
72
|
+
/** Main service invocation, or null if no invocation was observed. */
|
|
73
|
+
readonly invocationId: string | null;
|
|
74
|
+
/** Auxiliary invocation, or null if it was absent or never invoked. */
|
|
75
|
+
readonly retentionInvocationId: string | null;
|
|
76
|
+
}
|
|
77
|
+
/** Input from /dev/null, an existing file, or a caller-owned descriptor. */
|
|
78
|
+
type TaskInput = {
|
|
79
|
+
readonly type: 'null';
|
|
80
|
+
} | {
|
|
81
|
+
readonly type: 'file'; /** Absolute existing input path. */
|
|
82
|
+
readonly path: string;
|
|
83
|
+
} | {
|
|
84
|
+
readonly type: 'fd'; /** Nonnegative Unix descriptor, held open until start settles. */
|
|
85
|
+
readonly fd: number;
|
|
86
|
+
};
|
|
87
|
+
/** Output without decoding, in-memory capture, or automatic directory creation. */
|
|
88
|
+
type TaskOutput = {
|
|
89
|
+
readonly type: 'null';
|
|
90
|
+
} | {
|
|
91
|
+
readonly type: 'file'; /** Absolute output path. */
|
|
92
|
+
readonly path: string; /** Explicit append or truncate semantics. */
|
|
93
|
+
readonly mode: 'append' | 'truncate';
|
|
94
|
+
} | {
|
|
95
|
+
readonly type: 'fd'; /** Nonnegative Unix descriptor, held open until start settles. */
|
|
96
|
+
readonly fd: number;
|
|
97
|
+
};
|
|
98
|
+
/** All standard descriptors are explicit and excluded from persistent task identities. */
|
|
99
|
+
interface TaskIO {
|
|
100
|
+
/** Input source; this library does not create an interactive terminal. */
|
|
101
|
+
readonly stdin: TaskInput;
|
|
102
|
+
/** Binary standard output destination. */
|
|
103
|
+
readonly stdout: TaskOutput;
|
|
104
|
+
/** Binary standard error destination. */
|
|
105
|
+
readonly stderr: TaskOutput;
|
|
106
|
+
}
|
|
107
|
+
/** Explicit discard configuration for tasks whose output is not needed. */
|
|
108
|
+
declare const nullIO: TaskIO;
|
|
109
|
+
/** Whether the manager and cgroup provide evidence of finished descendant cleanup. */
|
|
110
|
+
type CleanupState = 'pending' | 'complete' | 'incomplete' | 'unknown';
|
|
111
|
+
/** Raw evidence retained by the specific service, with 64-bit values serialized as decimals. */
|
|
112
|
+
interface TaskEvidence {
|
|
113
|
+
/** Raw systemd Unit.ActiveState. */
|
|
114
|
+
readonly activeState: string;
|
|
115
|
+
/** Raw systemd Unit.SubState. */
|
|
116
|
+
readonly subState: string;
|
|
117
|
+
/** Raw systemd Service.Result, without inferred stop intent. */
|
|
118
|
+
readonly systemdResult: string;
|
|
119
|
+
/** CLD_* wait-status category reported by systemd. */
|
|
120
|
+
readonly execMainCode: number;
|
|
121
|
+
/** Exit code or signal number according to execMainCode. */
|
|
122
|
+
readonly execMainStatus: number;
|
|
123
|
+
/** Monotonic main-process start timestamp in microseconds; zero means absent. */
|
|
124
|
+
readonly execMainStartUsec: string;
|
|
125
|
+
/** Monotonic main-process exit timestamp in microseconds; zero means absent. */
|
|
126
|
+
readonly execMainExitUsec: string;
|
|
127
|
+
/** Retained active-entry evidence under Type=exec. */
|
|
128
|
+
readonly activeEnterUsec: string;
|
|
129
|
+
/** Current job ID, or zero when no job is pending. */
|
|
130
|
+
readonly jobId: number;
|
|
131
|
+
/** Manager-reported cgroup path, possibly empty after cleanup. */
|
|
132
|
+
readonly controlGroup: string;
|
|
133
|
+
/** Hierarchical cgroup population evidence; null when unavailable or unnecessary. */
|
|
134
|
+
readonly cgroupPopulated: boolean | null;
|
|
135
|
+
}
|
|
136
|
+
/** A program failure is a normal result, distinct from a transport or observation error. */
|
|
137
|
+
interface TaskResult {
|
|
138
|
+
/** Classification based on execution and exit evidence, never just the numeric status. */
|
|
139
|
+
readonly kind: 'exited' | 'signaled' | 'execution-failed' | 'not-executed' | 'unknown';
|
|
140
|
+
/** Program or systemd execution status, present only for CLD_EXITED. */
|
|
141
|
+
readonly exitCode: number | null;
|
|
142
|
+
/** Signal number, present only for CLD_KILLED or CLD_DUMPED. */
|
|
143
|
+
readonly signal: number | null;
|
|
144
|
+
/** Whether Type=exec activation was confirmed by retained manager evidence. */
|
|
145
|
+
readonly execution: 'confirmed' | 'not-confirmed';
|
|
146
|
+
/** Raw systemd result; timeout can describe runtime, stop, or cleanup escalation. */
|
|
147
|
+
readonly systemdResult: string;
|
|
148
|
+
/** Cleanup must be checked independently of a successful main-process exit. */
|
|
149
|
+
readonly cleanup: CleanupState;
|
|
150
|
+
}
|
|
151
|
+
/** A current state observation, not a durable event history. */
|
|
152
|
+
interface TaskSnapshot {
|
|
153
|
+
/** Reference captured from this observation. */
|
|
154
|
+
readonly reference: TaskReference;
|
|
155
|
+
/** Complete requires observed InvocationIDs for both task and retention service. */
|
|
156
|
+
readonly identity: 'complete' | 'incomplete';
|
|
157
|
+
/** Process phase; completed always requires confirmed cleanup. */
|
|
158
|
+
readonly phase: 'starting' | 'running' | 'cleaning' | 'completed';
|
|
159
|
+
/** Main PID is diagnostic only and never used as task identity. */
|
|
160
|
+
readonly mainPid: number;
|
|
161
|
+
/** True only for inactive/failed state without a pending job. */
|
|
162
|
+
readonly managerTerminal: boolean;
|
|
163
|
+
/** Current cleanup evidence. */
|
|
164
|
+
readonly cleanup: CleanupState;
|
|
165
|
+
/** Available main-exit or terminal evidence, possibly before cleanup finishes. */
|
|
166
|
+
readonly result: TaskResult | null;
|
|
167
|
+
/** Minimal raw properties needed to audit the classification. */
|
|
168
|
+
readonly evidence: TaskEvidence;
|
|
169
|
+
}
|
|
170
|
+
/** Absence does not prove that a task never ran; incomplete retention is also explicit. */
|
|
171
|
+
type LookupResult = {
|
|
172
|
+
readonly status: 'found'; /** Reference from this read. */
|
|
173
|
+
readonly reference: TaskReference; /** Current task state. */
|
|
174
|
+
readonly snapshot: TaskSnapshot;
|
|
175
|
+
} | {
|
|
176
|
+
readonly status: 'not-found'; /** Requested identity without secrets. */
|
|
177
|
+
readonly identity: TaskIdentity;
|
|
178
|
+
} | {
|
|
179
|
+
readonly status: 'incomplete'; /** Partial identity, safe to inspect and conservatively release. */
|
|
180
|
+
readonly reference: TaskReference;
|
|
181
|
+
};
|
|
182
|
+
/** Start acceptance means the manager queued the creation transaction, not successful exec. */
|
|
183
|
+
interface StartReceipt {
|
|
184
|
+
/** Acknowledged by the manager; subsequent program failure is a separate result. */
|
|
185
|
+
readonly outcome: 'accepted';
|
|
186
|
+
/** Requested identity, which is already known before the call. */
|
|
187
|
+
readonly identity: TaskIdentity;
|
|
188
|
+
/** Manager that accepted the request. */
|
|
189
|
+
readonly manager: ManagerIdentity;
|
|
190
|
+
/** Diagnostic path of the accepted job; jobs are not durable identities. */
|
|
191
|
+
readonly jobPath: string;
|
|
192
|
+
}
|
|
193
|
+
/** Local waiting controls; neither option changes manager-owned execution limits. */
|
|
194
|
+
interface WaitOptions {
|
|
195
|
+
/** Aborting affects this caller's wait only. */
|
|
196
|
+
readonly signal?: AbortSignal;
|
|
197
|
+
/** Positive integer local deadline, at most 2147483647 milliseconds. */
|
|
198
|
+
readonly timeoutMs?: number;
|
|
199
|
+
}
|
|
200
|
+
/** Stop acceptance and optional completion waiting are separate stages. */
|
|
201
|
+
interface StopOptions extends WaitOptions {
|
|
202
|
+
/** When true, await terminal cleanup after manager acceptance. Defaults to false. */
|
|
203
|
+
readonly wait?: boolean;
|
|
204
|
+
}
|
|
205
|
+
/** Local controls for the complete release sequence, independent of each D-Bus call. */
|
|
206
|
+
interface ReleaseOptions extends WaitOptions {
|
|
207
|
+
/** Total local release deadline including queueing; defaults to 5000 milliseconds. */
|
|
208
|
+
readonly timeoutMs?: number;
|
|
209
|
+
}
|
|
210
|
+
/** Successful stop submission or an already-completed instance. */
|
|
211
|
+
interface StopReceipt {
|
|
212
|
+
/** Already-completed does not submit any stop request. */
|
|
213
|
+
readonly outcome: 'accepted' | 'already-completed';
|
|
214
|
+
/** The exact reference checked before stopping. */
|
|
215
|
+
readonly reference: TaskReference;
|
|
216
|
+
/** Accepted stop job, or null for an already-completed instance. */
|
|
217
|
+
readonly jobPath: string | null;
|
|
218
|
+
/** Present when completion was awaited or already known. */
|
|
219
|
+
readonly completion: TaskSnapshot | null;
|
|
220
|
+
}
|
|
221
|
+
/** Explicit release affects manager retention, never application log files. */
|
|
222
|
+
interface ReleaseReceipt {
|
|
223
|
+
/** The auxiliary retention is no longer active. */
|
|
224
|
+
readonly released: true;
|
|
225
|
+
/** Both identities were already absent when release began. */
|
|
226
|
+
readonly alreadyReleased: boolean;
|
|
227
|
+
/** Whether both records were already garbage-collected at the final observation. */
|
|
228
|
+
readonly collected: boolean;
|
|
229
|
+
}
|
|
230
|
+
/** Stable categories for callers making recovery decisions. */
|
|
231
|
+
type ErrorCode = 'INVALID_INPUT' | 'UNSUPPORTED_ENVIRONMENT' | 'PERMISSION_DENIED' | 'MANAGER_UNAVAILABLE' | 'IDENTITY_CONFLICT' | 'START_REJECTED' | 'UNKNOWN_MUTATION_OUTCOME' | 'RESULT_UNAVAILABLE' | 'TASK_RUNNING' | 'WAIT_ABORTED' | 'WAIT_TIMEOUT' | 'DISCONNECTED' | 'OBSERVATION_FAILED';
|
|
232
|
+
/** Metadata safe for default error formatting and diagnostic output. */
|
|
233
|
+
interface ErrorDetails {
|
|
234
|
+
/** Public or transport operation where the failure was observed. */
|
|
235
|
+
readonly operation?: string;
|
|
236
|
+
/** Known delivery state; an unknown result must never authorize automatic retry. */
|
|
237
|
+
readonly mutationOutcome?: 'not-submitted' | 'rejected' | 'accepted' | 'unknown';
|
|
238
|
+
/** Acknowledged cleanup calls before a release error; may be positive even when a later call is unknown. */
|
|
239
|
+
readonly acceptedCleanupMutations?: number;
|
|
240
|
+
/** Stable detail such as unit-not-found or incomplete-identity. */
|
|
241
|
+
readonly reason?: string;
|
|
242
|
+
/** Random task ID only, never the serialized intent. */
|
|
243
|
+
readonly taskId?: string;
|
|
244
|
+
}
|
|
245
|
+
/** A redacted event correlated centrally with a task or connection flow. */
|
|
246
|
+
interface DiagnosticEvent {
|
|
247
|
+
/** Stable event name; no command, environment, or output content. */
|
|
248
|
+
readonly event: string;
|
|
249
|
+
/** Immutable correlation context captured at emission time. */
|
|
250
|
+
readonly context: {
|
|
251
|
+
/** Connection flow shared by this client's operations. */
|
|
252
|
+
readonly connection: {
|
|
253
|
+
readonly id: string;
|
|
254
|
+
};
|
|
255
|
+
/** Task scope, present for task operations. */
|
|
256
|
+
readonly task?: {
|
|
257
|
+
readonly id: string; /** Derived unit name. */
|
|
258
|
+
readonly unitName: string;
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
/** Stable failure category, when applicable. */
|
|
262
|
+
readonly code?: ErrorCode;
|
|
263
|
+
}
|
|
264
|
+
/** Explicit user-manager connection options; no automatic provisioning is performed. */
|
|
265
|
+
interface ConnectOptions {
|
|
266
|
+
/** Positive local D-Bus call deadline, at most 2147483647 ms; defaults to 5000. */
|
|
267
|
+
readonly requestTimeoutMs?: number;
|
|
268
|
+
/** Optional redacted event sink; sync throws and async rejections are isolated, never awaited. */
|
|
269
|
+
readonly onDiagnostic?: (event: DiagnosticEvent) => void;
|
|
270
|
+
}
|
|
271
|
+
/** The local client owns observation resources; systemd owns the task processes. */
|
|
272
|
+
interface TaskClient {
|
|
273
|
+
/** Bound manager identity for persistence and diagnosis. */
|
|
274
|
+
readonly manager: ManagerIdentity;
|
|
275
|
+
/** Submit exactly one creation attempt for a locally validated intention. */
|
|
276
|
+
start(intent: TaskIntent, io: TaskIO): Promise<StartReceipt>;
|
|
277
|
+
/** Read existing ownership evidence without starting or retrying anything. */
|
|
278
|
+
lookup(target: TaskIntent | TaskReference): Promise<LookupResult>;
|
|
279
|
+
/** Read the exact referenced instance or fail with explicit observation unavailability. */
|
|
280
|
+
inspect(reference: TaskReference): Promise<TaskSnapshot>;
|
|
281
|
+
/** Wait for cleanup, or return explicit incomplete cleanup on terminal manager failure. */
|
|
282
|
+
wait(target: TaskIntent | TaskReference, options?: WaitOptions): Promise<TaskSnapshot>;
|
|
283
|
+
/** Ask the manager to stop the referenced cgroup, optionally awaiting cleanup. */
|
|
284
|
+
stop(reference: TaskReference, options?: StopOptions): Promise<StopReceipt>;
|
|
285
|
+
/** Release only completed owned retention, preserving application files. */
|
|
286
|
+
release(reference: TaskReference, options?: ReleaseOptions): Promise<ReleaseReceipt>;
|
|
287
|
+
/** Close local connections and observers without stopping any task. */
|
|
288
|
+
disconnect(): Promise<void>;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Vstupní specifikace observed tasku rozšiřuje běžný příkaz o konečný stdin snapshot. */
|
|
292
|
+
interface ObservedTaskSpec extends TaskSpec {
|
|
293
|
+
/** Text se kóduje jako UTF-8, bytes se kopírují a null znamená explicitní /dev/null. */
|
|
294
|
+
readonly stdin: string | Uint8Array | null;
|
|
295
|
+
}
|
|
296
|
+
/** Serializovatelná identita tří jednotek, která neobsahuje příkaz ani vstupní data. */
|
|
297
|
+
interface ObservedTaskIdentity extends TaskIdentity {
|
|
298
|
+
/** Rozlišuje observed intent od základního dvoujednotkového kontraktu. */
|
|
299
|
+
readonly kind: 'observed';
|
|
300
|
+
/** Verze jednosměrného guardian framingu očekávaná tímto intentem. */
|
|
301
|
+
readonly protocolVersion: 1;
|
|
302
|
+
/** Odvozené jméno guardian služby, které caller nemůže zvolit. */
|
|
303
|
+
readonly guardianUnitName: string;
|
|
304
|
+
}
|
|
305
|
+
/** Připravený observed intent je bezpečný pro JSON round-trip, nikoliv automaticky pro logování. */
|
|
306
|
+
interface ObservedTaskIntent extends ObservedTaskIdentity {
|
|
307
|
+
/** Normalizovaný příkaz bez standardních descriptorů. */
|
|
308
|
+
readonly spec: TaskSpec;
|
|
309
|
+
/** Canonical base64 snapshot stdin; prázdný řetězec je prázdný input a null je /dev/null. */
|
|
310
|
+
readonly stdinBase64: string | null;
|
|
311
|
+
}
|
|
312
|
+
/** Přesná instance tří transient units svázaná s jedním systemd managerem. */
|
|
313
|
+
interface ObservedTaskReference extends ObservedTaskIdentity {
|
|
314
|
+
/** Manager, ze kterého pochází invocation evidence. */
|
|
315
|
+
readonly manager: ManagerIdentity;
|
|
316
|
+
/** Invocation ID payload služby, nebo null při neúplné topologii. */
|
|
317
|
+
readonly invocationId: string | null;
|
|
318
|
+
/** Invocation ID retention služby, nebo null při neúplné topologii. */
|
|
319
|
+
readonly retentionInvocationId: string | null;
|
|
320
|
+
/** Invocation ID guardian služby, nebo null při neúplné topologii. */
|
|
321
|
+
readonly guardianInvocationId: string | null;
|
|
322
|
+
}
|
|
323
|
+
/** Stav jedné procesní služby bez předstírání společného exit výsledku payloadu a guardianu. */
|
|
324
|
+
interface ObservedProcessSnapshot {
|
|
325
|
+
/** Main PID je pouze diagnostika a nikdy instance identity. */
|
|
326
|
+
readonly mainPid: number;
|
|
327
|
+
/** Terminální manager state vyžaduje inactive/failed bez čekajícího jobu. */
|
|
328
|
+
readonly managerTerminal: boolean;
|
|
329
|
+
/** Samostatný stav vyprázdnění cgroup této služby. */
|
|
330
|
+
readonly cleanup: CleanupState;
|
|
331
|
+
/** Exit evidence, jakmile ji manager skutečně poskytuje. */
|
|
332
|
+
readonly result: TaskResult | null;
|
|
333
|
+
/** Minimální raw evidence potřebná pro audit klasifikace. */
|
|
334
|
+
readonly evidence: TaskEvidence;
|
|
335
|
+
}
|
|
336
|
+
/** Současný snapshot payloadu i guardianu; nejde o historii událostí. */
|
|
337
|
+
interface ObservedTaskSnapshot {
|
|
338
|
+
/** Reference zachycená stejným čtením jako procesní evidence. */
|
|
339
|
+
readonly reference: ObservedTaskReference;
|
|
340
|
+
/** Complete vyžaduje neprázdná invocation ID všech tří jednotek. */
|
|
341
|
+
readonly identity: 'complete' | 'incomplete';
|
|
342
|
+
/** Completed vyžaduje terminální a vyčištěný payload i guardian. */
|
|
343
|
+
readonly phase: 'starting' | 'running' | 'cleaning' | 'completed';
|
|
344
|
+
/** Konzervativní agregace cleanup evidence obou procesních jednotek. */
|
|
345
|
+
readonly cleanup: CleanupState;
|
|
346
|
+
/** Výsledek uživatelského procesu zůstává oddělený od infrastruktury. */
|
|
347
|
+
readonly payload: ObservedProcessSnapshot;
|
|
348
|
+
/** Výsledek guardianu odhaluje fail-closed infrastrukturní selhání. */
|
|
349
|
+
readonly guardian: ObservedProcessSnapshot;
|
|
350
|
+
}
|
|
351
|
+
/** Lookup nikdy nedoplňuje chybějící jednotku ani neopakuje start. */
|
|
352
|
+
type ObservedLookupResult = {
|
|
353
|
+
readonly status: 'found'; /** Exact reference z tohoto čtení. */
|
|
354
|
+
readonly reference: ObservedTaskReference; /** Současný společný stav. */
|
|
355
|
+
readonly snapshot: ObservedTaskSnapshot;
|
|
356
|
+
} | {
|
|
357
|
+
readonly status: 'not-found'; /** Identita bez příkazu a stdin. */
|
|
358
|
+
readonly identity: ObservedTaskIdentity;
|
|
359
|
+
} | {
|
|
360
|
+
readonly status: 'incomplete'; /** Exact dostupná invocation evidence. */
|
|
361
|
+
readonly reference: ObservedTaskReference;
|
|
362
|
+
};
|
|
363
|
+
/** Jeden binární chunk doručený ze sekvenční bounded fronty guardianu. */
|
|
364
|
+
interface ObservedOutputEvent {
|
|
365
|
+
/** Původní payload descriptor bez slučování obou kanálů. */
|
|
366
|
+
readonly stream: 'stdout' | 'stderr';
|
|
367
|
+
/** Stabilní buffer, který už knihovna po callbacku nemění ani znovu nepoužije. */
|
|
368
|
+
readonly data: Uint8Array;
|
|
369
|
+
}
|
|
370
|
+
/** Callback je zároveň backpressure hranice; další frame čeká na jeho settlement. */
|
|
371
|
+
interface ObservedStartOptions {
|
|
372
|
+
/** Přijme každý output chunk přesně jednou během tohoto lokálního připojení. */
|
|
373
|
+
readonly onOutput: (event: ObservedOutputEvent) => void | Promise<void>;
|
|
374
|
+
}
|
|
375
|
+
/** Lokální připojení k živému outputu, které nevlastní manager task lifecycle. */
|
|
376
|
+
interface ObservedTaskObservation {
|
|
377
|
+
/** Settluje po čistém complete+EOF, explicitním detach, nebo observation chybě. */
|
|
378
|
+
readonly completion: Promise<{
|
|
379
|
+
readonly outcome: 'complete' | 'detached';
|
|
380
|
+
}>;
|
|
381
|
+
/** Idempotentně odpojí klienta; guardian dál drainuje a payload se nezastaví. */
|
|
382
|
+
detach(): Promise<void>;
|
|
383
|
+
}
|
|
384
|
+
/** Manager přijal jediný pokus a guardian zároveň potvrdil připravený output kanál. */
|
|
385
|
+
interface ObservedStartReceipt {
|
|
386
|
+
/** Přijetí není tvrzení o úspěšném exec nebo exit statusu payloadu. */
|
|
387
|
+
readonly outcome: 'accepted';
|
|
388
|
+
/** Veřejná identita bez spec a stdin. */
|
|
389
|
+
readonly identity: ObservedTaskIdentity;
|
|
390
|
+
/** Manager, který přijal atomickou tříjednotkovou transakci. */
|
|
391
|
+
readonly manager: ManagerIdentity;
|
|
392
|
+
/** Diagnostická cesta manager jobu, nikoliv durable identita. */
|
|
393
|
+
readonly jobPath: string;
|
|
394
|
+
}
|
|
395
|
+
/** Úspěšný start vrací zvlášť manager receipt a lokální output observation. */
|
|
396
|
+
interface ObservedStartResult {
|
|
397
|
+
/** Důkaz přijetí transakce a guardian readiness. */
|
|
398
|
+
readonly receipt: ObservedStartReceipt;
|
|
399
|
+
/** Klientem vlastněný stream, který lze kdykoliv lokálně odpojit. */
|
|
400
|
+
readonly observation: ObservedTaskObservation;
|
|
401
|
+
}
|
|
402
|
+
/** Lifecycle observed tasků sdílí manager a mutation pravidla se základním klientem. */
|
|
403
|
+
interface ObservedTaskClient {
|
|
404
|
+
/** Manager identity pevná po celou dobu tohoto spojení. */
|
|
405
|
+
readonly manager: ManagerIdentity;
|
|
406
|
+
/** Odešle jednu transakci a čeká na bounded guardian readiness. */
|
|
407
|
+
startObserved(intent: ObservedTaskIntent, options: ObservedStartOptions): Promise<ObservedStartResult>;
|
|
408
|
+
/** Čte všechny tři jednotky bez startu, retry nebo output adopce. */
|
|
409
|
+
lookup(target: ObservedTaskIntent | ObservedTaskReference): Promise<ObservedLookupResult>;
|
|
410
|
+
/** Čte přesnou referenced instanci nebo skončí explicitní chybou. */
|
|
411
|
+
inspect(reference: ObservedTaskReference): Promise<ObservedTaskSnapshot>;
|
|
412
|
+
/** Čeká na terminální manager stav obou služeb a vrátí samostatnou cleanup evidenci. */
|
|
413
|
+
wait(target: ObservedTaskIntent | ObservedTaskReference, options?: WaitOptions): Promise<ObservedTaskSnapshot>;
|
|
414
|
+
/** Zastaví payload cgroup; guardian dostane prostor dokončit drain. */
|
|
415
|
+
stop(reference: ObservedTaskReference, options?: StopOptions): Promise<ObservedStopReceipt>;
|
|
416
|
+
/** Uvolní retention až po potvrzeném cleanupu obou procesních služeb. */
|
|
417
|
+
release(reference: ObservedTaskReference, options?: ReleaseOptions): Promise<ReleaseReceipt>;
|
|
418
|
+
/** Odpojí všechny output observations a D-Bus bez zastavení tasků. */
|
|
419
|
+
disconnect(): Promise<void>;
|
|
420
|
+
}
|
|
421
|
+
/** Stop receipt zachovává observed snapshot místo dvoujednotkového výsledku. */
|
|
422
|
+
interface ObservedStopReceipt {
|
|
423
|
+
/** Already-completed nevytváří další manager mutation. */
|
|
424
|
+
readonly outcome: 'accepted' | 'already-completed';
|
|
425
|
+
/** Exact reference ověřená před zastavením. */
|
|
426
|
+
readonly reference: ObservedTaskReference;
|
|
427
|
+
/** Přijatý stop job, nebo null při již dokončeném tasku. */
|
|
428
|
+
readonly jobPath: string | null;
|
|
429
|
+
/** Snapshot je přítomný při wait:true nebo already-completed. */
|
|
430
|
+
readonly completion: ObservedTaskSnapshot | null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Connect explicitly to the local user manager; package import has no connection side effects. */
|
|
434
|
+
declare function connect(options?: ConnectOptions): Promise<TaskClient>;
|
|
435
|
+
/** Připojí stejný runtime až po guardian a systemd prerequisites preflightu. */
|
|
436
|
+
declare function connectObserved(options?: ConnectOptions): Promise<ObservedTaskClient>;
|
|
437
|
+
|
|
438
|
+
/** A recoverable API error with stable categories and redacted default formatting. */
|
|
439
|
+
declare class TaskError extends Error {
|
|
440
|
+
/** Stable category used by applications instead of parsing human-readable messages. */
|
|
441
|
+
readonly code: ErrorCode;
|
|
442
|
+
/** Safe delivery and identity metadata; never contains a command specification. */
|
|
443
|
+
readonly details: ErrorDetails;
|
|
444
|
+
/** Preserve the original cause for explicit access while keeping default output redacted. */
|
|
445
|
+
constructor(code: ErrorCode, message: string, details?: ErrorDetails, cause?: unknown);
|
|
446
|
+
/** Serialization deliberately excludes raw causes, stack contents, and request payloads. */
|
|
447
|
+
toJSON(): {
|
|
448
|
+
/** Stable error class name for serialized consumers. */
|
|
449
|
+
name: string;
|
|
450
|
+
/** Public recovery category. */
|
|
451
|
+
code: ErrorCode;
|
|
452
|
+
/** Redacted explanation safe for default logs. */
|
|
453
|
+
message: string;
|
|
454
|
+
/** Safe delivery evidence without raw causes. */
|
|
455
|
+
details: ErrorDetails;
|
|
456
|
+
};
|
|
457
|
+
/** Console and util.inspect must not expand a transport cause containing request data. */
|
|
458
|
+
[util.inspect.custom](): string;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Připraví úplný observed intent lokálně a zkopíruje mutable Uint8Array vstup. */
|
|
462
|
+
declare function prepareObserved(spec: ObservedTaskSpec): ObservedTaskIntent;
|
|
463
|
+
|
|
464
|
+
/** Prepare an intention locally; no connection, file opening, or process start occurs. */
|
|
465
|
+
declare function prepare(spec: TaskSpec): TaskIntent;
|
|
466
|
+
|
|
467
|
+
export { type CleanupState, type ConnectOptions, type DiagnosticEvent, type ErrorCode, type ErrorDetails, type LookupResult, type ManagerIdentity, type ObservedLookupResult, type ObservedOutputEvent, type ObservedProcessSnapshot, type ObservedStartOptions, type ObservedStartReceipt, type ObservedStartResult, type ObservedStopReceipt, type ObservedTaskClient, type ObservedTaskIdentity, type ObservedTaskIntent, type ObservedTaskObservation, type ObservedTaskReference, type ObservedTaskSnapshot, type ObservedTaskSpec, type ReleaseOptions, type ReleaseReceipt, type StartReceipt, type StopOptions, type StopReceipt, type SystemdProperty, type TaskClient, TaskError, type TaskEvidence, type TaskIO, type TaskIdentity, type TaskInput, type TaskIntent, type TaskOutput, type TaskReference, type TaskResult, type TaskSnapshot, type TaskSpec, type WaitOptions, connect, connectObserved, nullIO, prepare, prepareObserved };
|
|
@@ -321,3 +321,140 @@ retention reset, and retention stop boundaries. The unused raw object-path field
|
|
|
321
321
|
was removed; transport paths remain local to the D-Bus request that needs them.
|
|
322
322
|
The aggregate check passed 50 unit tests, 32 real integration tests, both builds,
|
|
323
323
|
and installed runtime/type verification including partial-cleanup error metadata.
|
|
324
|
+
|
|
325
|
+
## Observed-task feasibility checkpoint — 2026-09-17
|
|
326
|
+
|
|
327
|
+
The opt-in observed-task design passed its pre-API feasibility gate without
|
|
328
|
+
wrapping the payload process. One `StartTransientUnit` request created the existing
|
|
329
|
+
task and retention services plus a `Type=notify` guardian auxiliary service. The
|
|
330
|
+
task received stdout and stderr through the standard FD properties. The guardian
|
|
331
|
+
received the opposite stream endpoints and a client transport through the
|
|
332
|
+
`ExtraFileDescriptors` `a(hs)` property in the same auxiliary property array.
|
|
333
|
+
`LISTEN_PID`, `LISTEN_FDS`, and `LISTEN_FDNAMES` delivered the three descriptors as
|
|
334
|
+
`task-stdout`, `task-stderr`, and `observer` starting at descriptor 3.
|
|
335
|
+
|
|
336
|
+
The task uses `BindsTo=` and `After=` on the guardian. The guardian validates and
|
|
337
|
+
connects every descriptor before synchronously sending `READY=1` through
|
|
338
|
+
`systemd-notify`; a task held behind that readiness barrier did not execute early,
|
|
339
|
+
and a guardian exiting before readiness left the task unexecuted. A runtime
|
|
340
|
+
guardian SIGKILL stopped the task and its TERM-resistant detached descendant. The
|
|
341
|
+
feasibility prototype deliberately had no reverse stop propagation: after normal
|
|
342
|
+
task exit, explicit stop, or runtime timeout, the guardian drained buffered output
|
|
343
|
+
through EOF before exiting. The production implementation below later added
|
|
344
|
+
ordered stop propagation to cover payload cancellation before exec.
|
|
345
|
+
|
|
346
|
+
The feasibility checkpoint was originally recorded against the published
|
|
347
|
+
`@primafuture/socket-fdx@1.0.0`. A small development-only native helper created
|
|
348
|
+
three real Unix stream socket pairs and sent their six endpoints to the harness
|
|
349
|
+
over the existing bounded SCM_RIGHTS API. This avoided adding a speculative public
|
|
350
|
+
socket-pair contract before the systemd topology was proven. The helper and the
|
|
351
|
+
probe framing remain historical test fixtures, not runtime or public protocol
|
|
352
|
+
designs. The runnable harness now verifies the active development dependency
|
|
353
|
+
version and is retained as a regression gate alongside the production API tests.
|
|
354
|
+
|
|
355
|
+
The following real-manager scenarios passed:
|
|
356
|
+
|
|
357
|
+
- exact binary stdout/stderr routing, manager FD duplication, guardian evidence,
|
|
358
|
+
and retention of all three unit identities;
|
|
359
|
+
- deterministic readiness hold and pre-readiness guardian failure;
|
|
360
|
+
- guardian crash with fail-closed task and descendant cleanup;
|
|
361
|
+
- explicit observer detach and submitting-client SIGKILL followed by 32 MiB on
|
|
362
|
+
each output channel, with successful payload completion and no `EPIPE`;
|
|
363
|
+
- explicit task stop and manager runtime timeout with complete cgroup cleanup;
|
|
364
|
+
- a foreign guardian-name collision rejecting the transaction without executing
|
|
365
|
+
the payload.
|
|
366
|
+
|
|
367
|
+
One diagnostic pass completed all seven scenarios. The authoritative stress run
|
|
368
|
+
then completed ten consecutive rounds, 70 scenario executions, without retry,
|
|
369
|
+
descriptor-baseline drift, or a remaining `pf-systemd-tasks-observed-proof-*`
|
|
370
|
+
unit. It ran on Node 24.13.0, Linux x86-64 kernel 6.17.0-14-generic, glibc 2.42,
|
|
371
|
+
systemd user manager 257.9, and socket-fdx 1.0.0. Reproduce it with:
|
|
372
|
+
|
|
373
|
+
```sh
|
|
374
|
+
npm run feasibility:observed
|
|
375
|
+
npm run feasibility:observed -- --repeat 10
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
This result proves the topology on the recorded host only. It does not yet define
|
|
379
|
+
the public observed-task API, the final framing, a reconnect protocol, or support
|
|
380
|
+
for other systemd and Node versions. The next implementation dependency is an
|
|
381
|
+
explicitly owned socket-pair/adoption contract in `socket-fdx`; no package version
|
|
382
|
+
or runtime behavior changed at this checkpoint.
|
|
383
|
+
|
|
384
|
+
## Observed-task implementation — 2026-09-17
|
|
385
|
+
|
|
386
|
+
Version 1.1.0 implements the opt-in design under the separate
|
|
387
|
+
`@primafuture/systemd-tasks/observed` export. The original root export retains the
|
|
388
|
+
same four runtime values. Both thin ESM/CommonJS facades load one package-internal
|
|
389
|
+
runtime module, so errors from either API share the exact `TaskError` constructor.
|
|
390
|
+
Importing either facade remains side-effect free and does not load the native addon;
|
|
391
|
+
socket-fdx is dynamically loaded only when an observed start creates socket pairs
|
|
392
|
+
or the D-Bus adapter establishes a connection.
|
|
393
|
+
|
|
394
|
+
Observed intents use a distinct unit namespace and carry payload, retention, and
|
|
395
|
+
guardian names plus protocol version 1. Text stdin is copied as UTF-8, byte input is
|
|
396
|
+
copied directly, null remains `/dev/null`, and empty data remains distinct from null.
|
|
397
|
+
The canonical base64 value is fingerprinted and capped at 512 KiB so its expanded
|
|
398
|
+
manager-state representation remains safely below systemd's one-line serialization
|
|
399
|
+
boundary. Non-null data is
|
|
400
|
+
passed through systemd's `StandardInput=data` and `StandardInputData=ay`; there is
|
|
401
|
+
no input feeder or payload wrapper. Manager state can retain these sensitive bytes
|
|
402
|
+
until release.
|
|
403
|
+
|
|
404
|
+
The production guardian uses the already-proven three-unit topology and the public
|
|
405
|
+
socket-fdx socket-pair/adoption ownership contract. Its private stream protocol has
|
|
406
|
+
a ten-byte `SDTO`, version, kind, uint32be-length header, 64 KiB data frames, and a
|
|
407
|
+
256 KiB client receive budget. Exactly one empty ready frame precedes binary output,
|
|
408
|
+
and exactly one empty complete frame precedes clean EOF. The client rejects unknown,
|
|
409
|
+
out-of-order, partial, oversized, ancillary, or trailing data. Output callbacks are
|
|
410
|
+
awaited sequentially. Their failures are local observation failures and never become
|
|
411
|
+
implicit task-stop requests or diagnostic payloads.
|
|
412
|
+
|
|
413
|
+
JavaScript cannot cancel a callback promise that application code has already
|
|
414
|
+
started. Explicit detach therefore closes and unregisters the local socket and
|
|
415
|
+
settles the public observation immediately, without awaiting that promise. The
|
|
416
|
+
in-flight callback may settle later, but cannot change the detached outcome, invoke
|
|
417
|
+
another callback, delay `disconnect()`, or restore the output connection.
|
|
418
|
+
|
|
419
|
+
Guardian readiness has a fixed five-second budget. The guardian validates its three
|
|
420
|
+
named inherited sockets, establishes drain handlers, successfully invokes
|
|
421
|
+
`systemd-notify`, and only then emits the protocol ready frame. The payload starts
|
|
422
|
+
after and `BindsTo` the guardian. Payload stop requests use systemd's
|
|
423
|
+
`PropagatesStopTo=` and reverse ordering, so the payload is terminal before the
|
|
424
|
+
guardian receives SIGTERM. An invoked payload retains strict socket EOF as the only
|
|
425
|
+
drain-completion signal, including while a slow callback applies backpressure. For
|
|
426
|
+
a cancelled pre-exec payload, the guardian confirms the still-empty `InvocationID`
|
|
427
|
+
through `/usr/bin/systemctl` before closing manager-owned write endpoints that can
|
|
428
|
+
never produce data. Client detach or connection loss switches both inputs to
|
|
429
|
+
flowing discard mode, removing observer backpressure while retaining real EOF as
|
|
430
|
+
the process-lifecycle boundary. There is no reconnect, replay, or live-stream adoption.
|
|
431
|
+
|
|
432
|
+
The original D-Bus adapter now accepts one primary service and an ordered auxiliary
|
|
433
|
+
list, while retaining a single request, collision mode `fail`, descriptor lifetime,
|
|
434
|
+
and mutation-outcome rules. One client core owns the transport, manager identity,
|
|
435
|
+
in-flight read deduplication, per-identity mutation queues, notification hub,
|
|
436
|
+
diagnostics, and local output handles. The two-unit and three-unit codecs share that
|
|
437
|
+
core. Observed lookup treats every partial permutation as incomplete, references
|
|
438
|
+
all three invocation IDs, and never fabricates a missing role. Completion and
|
|
439
|
+
release require terminal empty cgroups for both payload and guardian.
|
|
440
|
+
|
|
441
|
+
Development is pinned to the exact published
|
|
442
|
+
`npm:@primafuture/socket-fdx@1.1.0` release. The lockfile records its registry
|
|
443
|
+
tarball and SRI integrity. The installed-package smoke uses no consumer override
|
|
444
|
+
and verifies that one canonical socket-fdx instance is loaded. No npm publication
|
|
445
|
+
of this package is part of this changeset.
|
|
446
|
+
|
|
447
|
+
The post-review local acceptance run completed 67 deterministic unit tests, 43 real
|
|
448
|
+
systemd integration scenarios, and installed ESM/CommonJS runtime and TypeScript
|
|
449
|
+
consumer smoke tests. The earlier historical feasibility stress run remains ten
|
|
450
|
+
rounds (70 scenarios), as recorded above. The installed consumer verified the
|
|
451
|
+
unchanged root runtime export set, the separate observed entrypoint, canonical `TaskError` identity across both
|
|
452
|
+
entrypoints and mixed ESM/CommonJS loading, lazy native loading, and exactly one
|
|
453
|
+
socket-fdx 1.1.0 runtime instance. Real-manager coverage includes binary, empty,
|
|
454
|
+
and exact maximum-size stdin, retention-start failure, separate binary streams, async
|
|
455
|
+
callback backpressure, explicit detach, submitting-client SIGKILL with 32 MiB per
|
|
456
|
+
channel, drain-preserving explicit stop, cross-client cancellation before payload
|
|
457
|
+
exec, guardian fail-closed behavior, collision and incomplete topology handling,
|
|
458
|
+
unknown mutation outcomes, exact invocation ownership, release, and empty cgroups.
|
|
459
|
+
No output bytes, stdin bytes, or callback error payloads are emitted by library
|
|
460
|
+
diagnostics.
|