@primafuture/systemd-tasks 1.0.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 ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 — 2026-09-07
4
+
5
+ - Add explicit preparation, single-attempt submission, lookup, inspection, waiting,
6
+ stopping, release, and disconnect for systemd user-manager tasks.
7
+ - Preserve literal arguments, explicit environment entries, and binary Unix FD I/O
8
+ with defined borrowed/file ownership and append/truncate semantics.
9
+ - Retain results through client crashes using an auxiliary transient oneshot;
10
+ verify manager and invocation identities before lifecycle mutations.
11
+ - Separate acceptance, execution evidence, exit result, and descendant cleanup;
12
+ retain unknown mutation outcomes without automatic retries.
13
+ - Provide independent cancellable waiters, manager-owned escalation, redacted
14
+ correlated diagnostics, and a CPUWeight/IOWeight property allowlist.
15
+ - Ship ESM/CommonJS builds and declarations, native FD dependency licensing,
16
+ runnable recovery examples, and real plus deterministic lifecycle tests.
17
+ - Reject unavailable internal retention executables during connection preflight,
18
+ including missing paths, non-regular files, and missing execute permissions.
19
+ - Replace upstream usocket with the pinned `@primafuture/socket-fdx@1.0.0`
20
+ alias, shipping a prebuilt FD transport without consumer overrides or install
21
+ scripts. Require Node 24.13.0+ and Linux x64 with glibc 2.28+.
22
+ - Classify native platform, ABI, and missing-binary failures as unsupported
23
+ environment; exercise overlapping stop/release and repeated FD-bearing success
24
+ and rejection paths through real connections.
25
+ - Give release an independent configurable local deadline and cancellation,
26
+ preserving acknowledged cleanup mutations across observation timeouts and later
27
+ rejected or uncertain cleanup steps through explicit progress metadata.
28
+ - Isolate asynchronous diagnostic sink rejections without delaying task operations.
29
+
30
+ Verified on Node 24.13.0 and systemd 257.9 on Linux x86-64 with cgroup v2.
31
+ Retention does not survive manager shutdown or reboot.
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 PrimaFuture.cz s.r.o.
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,250 @@
1
+ # @primafuture/systemd-tasks
2
+
3
+ Run one-shot Linux processes owned by the **systemd user manager**, with literal
4
+ arguments, binary file-descriptor I/O, manager-owned timeouts, and recoverable
5
+ results after the Node client exits or crashes.
6
+
7
+ Version 1.0.0 requires Node 24.13.0+, Linux x64 with glibc 2.28+, systemd 257+,
8
+ and cgroup v2. The verified environment
9
+ is Node 24.13.0, systemd 257.9-0ubuntu2.4, Linux 6.17.0-14-generic, x86-64.
10
+ Other versions satisfying those minimums are not independently verified.
11
+
12
+ ## Installation
13
+
14
+ ```sh
15
+ npm install @primafuture/systemd-tasks
16
+ ```
17
+
18
+ The pinned native dependency `@primafuture/socket-fdx@1.0.0` is installed under
19
+ the npm alias `usocket` and supplies its compatible `USocket` adapter. It ships a
20
+ prebuilt native binary: ordinary installation needs no compiler, Python, or install
21
+ scripts. Consumers do not need `overrides`. The dependency is pinned to its stable release;
22
+ musl Linux and other architectures are outside its current support. The JavaScript D-Bus stack
23
+ (`dbus-next@0.10.2` and its dependencies) is bundled; licenses are recorded in
24
+ [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
25
+
26
+ An existing local user bus at `/run/user/<uid>/bus`, a reachable systemd user
27
+ manager, Unix FD negotiation, and `/usr/bin/true` are required. `connect()` checks
28
+ that the retention executable resolves to a regular executable file before opening
29
+ the bus, then verifies the manager environment. It fails explicitly; the library does not provision a manager,
30
+ enable linger, request elevated privileges, or fall back to child_process.
31
+ Importing either the ESM or CommonJS entrypoint does not connect or start a process.
32
+
33
+ ## Basic execution
34
+
35
+ ```ts
36
+ import * as tasks from '@primafuture/systemd-tasks';
37
+
38
+ const intent = tasks.prepare({
39
+ executable: '/usr/bin/printf',
40
+ argv: ['%s\n', 'Literal $HOME, spaces, and Unicode: žluťoučký'],
41
+ cwd: '/tmp',
42
+ env: {},
43
+ runtimeLimitMs: 5000,
44
+ stopGraceMs: 500,
45
+ });
46
+ const client = await tasks.connect();
47
+ try {
48
+ await client.start(intent, {
49
+ ...tasks.nullIO,
50
+ stdout: { type: 'file', path: '/tmp/task-output.log', mode: 'append' },
51
+ });
52
+ const completed = await client.wait(intent, { timeoutMs: 10000 });
53
+ console.log(completed.result);
54
+ if (completed.phase === 'completed') {
55
+ // Persist any required result before discarding manager retention.
56
+ await client.release(completed.reference);
57
+ }
58
+ } finally {
59
+ await client.disconnect();
60
+ }
61
+ ```
62
+
63
+ `executable` and `cwd` must be absolute; no PATH lookup or shell parsing is performed.
64
+ `argv` excludes argv[0]. Empty arguments, `$`, `%`, quotes, and Unicode remain
65
+ literal. Every task must explicitly choose `runtimeLimitMs` (positive integer
66
+ milliseconds or `null`) and `stopGraceMs` (positive integer milliseconds).
67
+
68
+ `env` is an explicit map. The library removes names from the manager's inherited
69
+ environment that are absent from this map, using a snapshot immediately before
70
+ submission. Systemd may add execution metadata such as `INVOCATION_ID`. Concurrent
71
+ external changes to manager configuration are outside this guarantee.
72
+
73
+ ## Lifecycle and recovery
74
+
75
+ | Operation | Contract |
76
+ | --- | --- |
77
+ | `prepare(spec)` | Pure local validation, random identity, frozen serializable intent. No I/O or execution. |
78
+ | `connect(options?)` | Explicit local connection, environment checks, immutable manager identity. |
79
+ | `start(intent, io)` | One creation attempt. A receipt means manager acceptance, not successful exec. |
80
+ | `lookup(intentOrReference)` | Observe existing work: `found`, `not-found`, or `incomplete`. Never starts anything. |
81
+ | `inspect(reference)` | Snapshot of the exact instance; missing evidence is an error. |
82
+ | `wait(intentOrReference, options?)` | Wait for terminal manager state and report cleanup evidence. |
83
+ | `stop(reference, options?)` | Request manager stop; `wait: true` additionally waits for cleanup. |
84
+ | `release(reference, options?)` | Refuse live tasks, then discard owned result retention. Never deletes logs. |
85
+ | `disconnect()` | Release local socket/listeners only; tasks keep running. Idempotent. |
86
+
87
+ Save the **intent before calling start**, then save an observed reference and final
88
+ result as they become available. Intents contain the command and environment,
89
+ possibly secrets. Use application-appropriate permissions and durable storage.
90
+ A prepared intent does not prove that submission ever occurred.
91
+
92
+ A second process can deserialize an intent, connect, and call `lookup` or `wait`.
93
+ References bind the task UUID, specification fingerprint, manager scope, UID,
94
+ machine/boot/bus identity, manager bus owner, and both service InvocationIDs.
95
+ Missing InvocationIDs are explicitly `null`; an incomplete reference cannot
96
+ silently adopt a later invocation. Waiting on an intent pins IDs as they appear.
97
+ PIDs are diagnostic information, never ownership identity.
98
+
99
+ `not-found` means that no owned record is currently observable in the reachable
100
+ manager. It does **not** prove that the command never executed. `incomplete` means
101
+ only the retention unit remains and the main result is unavailable. A found
102
+ snapshot can itself have incomplete invocation evidence, for example after
103
+ retention initialization prevented execution.
104
+
105
+ Start errors expose `TaskError.details.mutationOutcome`:
106
+
107
+ | Outcome | Recovery meaning |
108
+ | --- | --- |
109
+ | `not-submitted` | No creation request was sent by this operation; file-opening effects may exist. |
110
+ | `rejected` | A manager error reply was received; inspect possible partial records. |
111
+ | `unknown` | Delivery or acceptance cannot be established. Reconnect and look up the saved identity. |
112
+ | `accepted` | Acceptance was confirmed; a later local stop wait can still fail or be cancelled. |
113
+
114
+ **Never automatically retry a start with an unknown outcome**, even when lookup
115
+ returns `not-found`. Local duplicate protection lasts only for that client; this
116
+ is not a durable exactly-once ledger. Creating another intent is a new execution
117
+ and requires an application decision. There is no automatic reconnect or retry.
118
+
119
+ References guard accidental collisions and stale instances. They are not a security
120
+ boundary against equally privileged processes. Systemd has no conditional
121
+ "stop only if InvocationID equals X" method; read/validate/mutate has a race if an
122
+ external actor replaces a unit between those steps. Do not modify library-owned
123
+ units concurrently through another controller.
124
+
125
+ ## Results, timeouts, and cleanup
126
+
127
+ `TaskSnapshot.result` is a normal result for application failures, signals, and
128
+ execution failures. It contains `kind`, `exitCode`, `signal`, `execution`,
129
+ `systemdResult`, and `cleanup`. The snapshot also preserves raw exit, timestamp,
130
+ job, and cgroup evidence. A program returning 203 differs from systemd failing to
131
+ exec with status 203; classification uses retained `Type=exec` activation evidence.
132
+
133
+ `phase: 'completed'` requires terminal manager state, no pending job, and confirmed
134
+ cgroup cleanup. Main-process exit alone is insufficient. `wait()` can return a
135
+ terminal snapshot with `cleanup: 'incomplete'` or `'unknown'`; inspect that field
136
+ and do not treat it as successful completion or attempt release. Such a snapshot
137
+ remains in `phase: 'cleaning'`.
138
+
139
+ Runtime limits, SIGTERM, stop grace, SIGKILL escalation, and descendant cleanup are
140
+ owned by systemd. They continue after client death. `Result=timeout` can mean a
141
+ runtime deadline, stop escalation, or descendant cleanup escalation; the library
142
+ preserves it without claiming which occurred. Descendants remaining in the task
143
+ cgroup are covered, including ordinary setsid descendants. Privileged processes
144
+ that escape the cgroup are outside the guarantee.
145
+
146
+ `wait`/`stop` options `signal` and `timeoutMs` affect only that caller's local wait.
147
+ They do not stop a task, cancel an accepted stop, or cancel other waiters. Local
148
+ timeouts are positive integers up to 2147483647 ms. Stop cancellation while its
149
+ request is in flight reports unknown delivery; after an acknowledgment, the error
150
+ retains `mutationOutcome: 'accepted'`.
151
+
152
+ ## Binary I/O and ownership
153
+
154
+ All three channels are explicit. `nullIO` provides EOF stdin and discarded output.
155
+
156
+ | Channel value | Ownership and behavior |
157
+ | --- | --- |
158
+ | `{ type: 'null' }` | Selects systemd's null channel mode; the client opens no descriptor. |
159
+ | `{ type: 'file', path }` for stdin | Existing regular file, opened for reading before submission. |
160
+ | `{ type: 'file', path, mode: 'append' \| 'truncate' }` for output | Regular file; parent directory must exist. Creation/truncation is not transactional. |
161
+ | `{ type: 'fd', fd }` | Borrowed Unix FD; the caller must keep it open until start settles. Never closed by the library. |
162
+
163
+ The transport passes descriptors directly to systemd; the client never decodes or
164
+ buffers process output. File output survives client death. Successful submission
165
+ does not mean the task has finished using its manager-held descriptor.
166
+
167
+ Identical output paths and aliases of the same inode with matching modes share one
168
+ open file description and offset. Conflicting modes are rejected. Opens and alias
169
+ checks precede truncation; a later manager rejection or exec failure can still leave
170
+ created or truncated files. Truncating an input/output alias is rejected. Existing
171
+ symlinks are followed; this is not a sandbox for untrusted paths. Use borrowed FDs
172
+ when your application needs stricter opening policies or non-regular files.
173
+
174
+ Borrowed FDs share underlying offsets and flags with the application. Unix pipes
175
+ retain normal backpressure, EOF, and EPIPE behavior. Closing a reader can break task
176
+ output. Reconnecting does not reconstruct lost pipe bytes or create stream history.
177
+ The library does not own application streams and never waits for task-held pipe EOF
178
+ when disconnecting. Regular files are the durable-output option. Release leaves
179
+ those files untouched.
180
+
181
+ ## Retention and operational limits
182
+
183
+ Each task uses two transient services: the actual `Type=exec` service and a short
184
+ `/usr/bin/true` oneshot with `RemainAfterExit=yes`. Retention becomes active before
185
+ the user command runs. It keeps the completed service referenced without a resident
186
+ helper process. Both records consume manager memory until explicit release; failed
187
+ records are reset during release. Garbage collection may follow asynchronously,
188
+ which is reflected by `ReleaseReceipt.collected`.
189
+
190
+ `release(reference, { timeoutMs, signal })` has its own total local deadline,
191
+ including time queued behind another mutation; it defaults to 5000 ms.
192
+ `connect({ requestTimeoutMs })` independently limits each individual D-Bus request,
193
+ not the sum of release's reads and mutations. Increasing the release deadline does
194
+ not relax the per-request deadline. Cancellation does not undo manager mutations.
195
+ Release errors include `acceptedCleanupMutations`, the number of acknowledged
196
+ reset/stop calls during that attempt. If earlier cleanup was accepted but a later
197
+ step is explicitly rejected or not submitted, `mutationOutcome` remains `accepted`:
198
+ the manager has already acknowledged part of cleanup. If a later step has unknown delivery,
199
+ the outcome is `unknown` and the positive count still preserves the earlier facts.
200
+ Neither acceptance nor the count proves completion of the whole release sequence.
201
+ Reconcile the reference before continuing cleanup.
202
+
203
+ Retention is not persistent storage across manager restart, logout that stops the
204
+ user manager, bus replacement, or reboot. Store final results yourself. External
205
+ service manipulation can invalidate evidence. The library does not enable linger,
206
+ recreate missing tasks, implement a daemon, or promise survival when the manager
207
+ itself stops. Connection failure is distinct from missing units.
208
+
209
+ The only advanced properties allowed in 1.0.0 are `CPUWeight` and `IOWeight`, each
210
+ with signature `'t'` and an integer value from 1 to 10000. Duplicates, unknown
211
+ properties, and lifecycle overrides are rejected before mutation. Kernel/controller
212
+ configuration determines the practical scheduling effect of those weights.
213
+
214
+ Optional `connect({ onDiagnostic })` receives structured events correlated by
215
+ `context.connection` and `context.task`. No argv, environment, output, or raw error
216
+ cause is logged implicitly. Synchronous sink exceptions and rejected promises or
217
+ thenables do not change task outcomes; asynchronous sinks are never awaited.
218
+ `TaskError.cause` is explicitly accessible and may contain sensitive upstream text;
219
+ default error inspection and JSON formatting are redacted.
220
+
221
+ ## Development and verification
222
+
223
+ ```sh
224
+ npm ci --prefix "$PWD"
225
+ npm run check
226
+ ```
227
+
228
+ `check` runs strict typechecking, unit tests, real systemd integration tests, dual
229
+ ESM/CJS build, and tarball runtime/type smoke tests. `prepublishOnly` invokes the
230
+ same chain. Integration tests fail explicitly when prerequisites are missing.
231
+ Tests create unique identities and clean only their recorded task/retention pairs.
232
+ They do not restart the user manager or change host configuration.
233
+
234
+ Runnable repository examples after `npm run build`:
235
+
236
+ ```sh
237
+ npx tsx examples/basic.ts
238
+ npx tsx examples/durable-start.ts
239
+ # Use the printed record directory from the preceding command:
240
+ npx tsx examples/durable-reconnect.ts /absolute/path/to/record-directory
241
+ npx tsx examples/timeout-stop.ts
242
+ ```
243
+
244
+ The two durable examples are independent processes. The first fsyncs its intent
245
+ before submission and disconnects; the second fsyncs the result before release.
246
+ Generated records stay under `.artifacts/examples` and are not published.
247
+
248
+ See [implementation decisions and acceptance evidence](docs/implementation-decisions.md)
249
+ for the feasibility experiments, T01–T18 mapping, exact versions, simulated failure
250
+ boundaries, and unverified environments.