@orchestraworks/worker 0.0.134

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 (4) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +518 -0
  3. package/bin/worker.js +80 -0
  4. package/package.json +30 -0
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 Living Computers
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,518 @@
1
+ # The worker
2
+
3
+ One Go program runs inside every machine and speaks the Factory Worker Protocol to the
4
+ platform. This tree is that program, and it is also the `@orchestraworks/worker` npm package: the launcher on
5
+ the path, and one per-platform package carrying the binary for the machine it is installed on.
6
+
7
+ The worker exposes no API of its own. Every route it calls is the platform's.
8
+
9
+ ## The tree
10
+
11
+ | path | what it holds |
12
+ |---|---|
13
+ | `cmd/worker/` | the one binary: `run`, `install`, `enroll`, `check`, `version` |
14
+ | `identity/` | the four identity exchanges — claim, refresh, reattach-challenge, reattach — the holder key, and the loop that keeps the machine token alive |
15
+ | `broker/` | the protocol on the message broker: the connection, the register on every connect, the heartbeat and the status cadence, the pull of this machine's own work consumer, and the publisher every other package sends through |
16
+ | `env/` | the machine-side environment the platform composes onto a launch, read from the names the contract declares |
17
+ | `facts/` | what this build knows about the four harness families: the program to install and the digest that authenticates it, the environment each family is handed, the templates it seeds, and every location its content lands in — embedded in the binary, so one release digest covers the facts and the program that reads them |
18
+ | `harness/` | the only place family-specific knowledge lives: the launch plan, the content layout, the store lookup, the mid-turn message each family accepts, and the installer that puts the pinned Node and a family's programs under the worker root |
19
+ | `process/` | the agent's setup and the harness process behind each session placement: the start, the stop and the last save a remove asks for, the startup fence over what a previous run left, and the stop of everything when the worker is told to stop |
20
+ | `stream/` | the queue: every placement message and every prompt, steer and cancel pulled off this machine's work consumer, the ACP client that opens a session's harness conversation and runs its turns, each turn's events batched to the platform and published live, and the answer every placement message is owed |
21
+ | `mount/` | the volumes the launch declares, placed on the lanes that mount them: the home volume first, because anything written under a mount point before the mount is hidden by it |
22
+ | `network/` | the transparent redirect on the virtual-machine lanes, as the iptables rules the launch describes: what the worker's own uid reaches directly, and what every uid on the machine reaches through the egress edge |
23
+ | `shim/` | the in-guest transparent proxy: the listener the redirect lands on, the recovery of the destination each connection was actually for, the hop to the egress edge under the machine-identity proof, the WinDivert half that is the Windows redirect, and the loopback feed a separate shim reads this machine's token from |
24
+ | `egress/` | what puts that proxy in place on the virtual-machine lanes: the edge's certificate authority in this machine's trust store, the inbound firewall rule Windows needs, the redirect itself, and the state line an operator reads off the machine's console |
25
+ | `ingress/` | the machine's half of the preview tunnel: the connection this worker holds open to the preview gateway, the challenge and the signed upgrade that authenticate it, and the loopback pipe that carries each viewer request to a port this machine declared and refuses every other |
26
+ | `termination/` | the cloud's notice that this machine is being reclaimed, delivered to the broker so the next status report carries it |
27
+ | `paths/` | which native place a logical path denotes, and when two logical paths denote one place — pinned against the platform's own answer by committed vectors |
28
+ | `service/` | `worker run` under this operating system's service manager, in one of two scopes: a system or user `systemd` unit, a Windows service or logon task, a launchd daemon or launch agent |
29
+ | `file/` | everything that moves bytes onto this machine and back off it: the install of the skills, memory, instructions and model settings an agent placement's apply carries, against the install journal, the capture that offers the machine's declared roots back to the platform, the restore that lays a previous machine's capture down before a manifest is installed over it, and the self-update that replaces this worker's own release package |
30
+ | `script-harness/` | the first-party `script` harness program, which is the one family the worker package carries rather than installs |
31
+ | `bin/worker.js` | the npm launcher: it runs the binary out of the per-platform package installed for this machine, stays in front of it, and forwards the signal a service manager ends it with. Its own tests sit beside it and are not packed |
32
+ | `npm/<platform>/` | the five per-platform packages, one per release target: the binary, and the `script` harness copied in beside it at build time |
33
+
34
+ `../fwp` is the protocol. Every wire shape comes from `github.com/livingcomputers/fwp/go/wire`
35
+ through a relative `replace`, so the program and the conformance suite compile against the exact
36
+ shapes the schema ships, and nothing here re-declares one.
37
+
38
+ ## The commands
39
+
40
+ - **`worker run`** claims this machine's identity — or reattaches, where its row is already bound to
41
+ the key this machine holds — and then serves the machine. `run` hands the held identity, with the
42
+ broker credential the claim co-issued, to the registrar seam in `cmd/worker/run.go`, which the
43
+ `broker` package fills: it dials, registers the machine on every connect, pulls the machine's own
44
+ work consumer and reports on the cadence.
45
+
46
+ Before any of that, `run` starts what the launch describes: it places the declared volumes, puts
47
+ the in-guest egress in place where the launch names an intercept set, opens the egress shim's token
48
+ feed, and — on a machine the cloud can reclaim — watches for the notice that it is going away. Each
49
+ is started only where it is described: a Kubernetes pod places its own volumes and its egress
50
+ sidecar installs its own redirect, so a launch there names neither. If one of those duties is
51
+ installed and then dies, `run` ends rather than serving a machine the platform counts as healthy
52
+ with no route off it; the service manager starts the worker again.
53
+
54
+ `--service` is how the process is hosted rather than what it does: on Windows it hands the body to
55
+ the service control manager, which kills a service that does not answer its handshake, and on the
56
+ other operating systems it is refused by name, because systemd and launchd supervise a foreground
57
+ process from outside.
58
+
59
+ On a machine that hosts a harness, `run` also builds the three lanes that answer the platform's
60
+ verbs: `process`, which holds the placements, `stream`, which carries their turns, and `file`,
61
+ which installs what an agent is made of and captures what its work left behind. The three are
62
+ built before the broker, because the broker's configuration carries their handlers, and each
63
+ reaches it afterwards through the deferred link in `cmd/worker/serve.go`.
64
+
65
+ Once the register has been answered, `run` starts the capture cadence and, where the platform named
66
+ a version other than the one this process is running, the self-update: the release package is
67
+ fetched from the npm registry when this machine can reach one and from the platform when it cannot,
68
+ checked against the digest the register answer named, unpacked over the release package directory,
69
+ and the process restarted — but only once no turn has been open for five seconds together, because one
70
+ open turn per machine is the platform's rule and killing one would lose its answer. A machine whose
71
+ turns never leave that gap keeps running the version it has until some later start picks the unpacked
72
+ one up.
73
+ - **`worker install --role worker|image|laptop --harness <families> --yes [--root <dir>] [--service=user|none]`**
74
+ installs the worker on this machine and starts its service. Every answer comes from a flag: a missing
75
+ one is an error and never a default. It installs the harness half itself — the pinned Node, then each
76
+ named family's programs from the facts file's own pins, with the tarball checked against the pinned
77
+ digest before anything is unpacked — and then registers the service. The per-operating-system service
78
+ forms fill the installer seam in `cmd/worker/install.go`; a build with none refuses rather than
79
+ reporting an install it did not perform.
80
+
81
+ **`--service` says which session the worker is registered in, and neither of its two values needs any
82
+ privilege.** `user` registers it in the invoking account's own session: a launch agent under
83
+ `~/Library/LaunchAgents` on macOS, a `systemd --user` unit under `~/.config/systemd/user` on Linux, a
84
+ scheduled task the account's logon starts on Windows. `none` installs the files and registers nothing,
85
+ for the one host that has no service manager — a container image, whose own command is the worker —
86
+ and `false` is that same answer under its older spelling. The default is `user`. An install run as
87
+ root is refused, because it would put the worker's root under root's home and run every harness as
88
+ root; there is no privileged install to redirect it to, and `--service=system` is refused with the
89
+ same answer. Nothing on a machine somebody owns needs privilege: network enforcement is outside the
90
+ machine, at the egress edge and the directional security group.
91
+
92
+ **The machine-wide service is the image role's, and the role is the whole of what asks for it.** An
93
+ `image` gets it — a property list under `/Library/LaunchDaemons`, a unit under `/etc/systemd/system`,
94
+ a Windows service — because the platform builds the image with nobody logged in to it and nobody
95
+ typing the command, and the builder is already root. No flag selects that scope, on any role.
96
+
97
+ `--root` follows the scope the same way: `~/.sfdk/worker` (`%LOCALAPPDATA%\sfdk\worker` on Windows) for
98
+ a `user` install, `/var/lib/sfdk/worker` (`%ProgramData%\orchestra\worker`) for the other two. A `user`
99
+ install writes its log under that root, at `log/worker.log`, because no service manager in that scope
100
+ keeps one a person can read.
101
+
102
+ What the role decides is when the service runs — a `worker` starts now and at every boot, an `image`
103
+ being baked starts at the first boot of a machine made from it and claims no identity now, and a
104
+ `laptop` starts when a person enrolls a machine and says so. An `image` install also refuses, before
105
+ it installs anything, when a bootstrap file already sits at any path the run would read: an image is
106
+ copied onto every machine made from it, so a baked bootstrap would hand one machine's identity to all
107
+ of them.
108
+
109
+ Each form carries one thing its machine would otherwise be wrong without. A macOS launch DAEMON — the
110
+ image role's form, which a person never reaches — names the account that ran the install, because
111
+ launchd's system domain runs a daemon as root otherwise, and a harness runs as whoever this worker is;
112
+ the worker's own root and the daemon's log file are handed to that account in the same step, so the
113
+ service can read the owner-only bootstrap the root install just wrote, write its state file beside it,
114
+ and say in its log why it did not start. A launch AGENT names no account: it is
115
+ already the session's. The Windows service is registered to restart itself three times at two seconds,
116
+ on a clean non-zero exit as well as a crash — the control manager restarts nothing it was not told to,
117
+ and this worker exits non-zero deliberately to be started again on a release its self-update put in
118
+ place — and the logon task carries the task scheduler's nearest equivalent, three restarts a minute
119
+ apart.
120
+
121
+ A `worker` install run as root on Linux refuses outright where the `agent` account a harness is
122
+ dropped to is not on the machine, and names the command that creates it: a root worker runs its
123
+ harness as root without that account, so this machine would register with no harness capability,
124
+ report healthy, and refuse every conversation sent to it, having reported an install that succeeded.
125
+ Creating a system account on a machine somebody else owns is the operator's to do, so the install
126
+ names it rather than making it. One `worker` install can still be root's — `--service=none` on a
127
+ machine whose own command is the worker — and every other one is refused as root before it gets this
128
+ far. An `image` carries the account already, and an install that registers a service in the invoking
129
+ account's session has no second account at all: the harness runs as the person who installed it.
130
+
131
+ A family a placement names and this machine lacks is installed on the preparation path instead,
132
+ before its harness starts, so a first provision is not slowed by a download of hundreds of megabytes
133
+ that nobody asked for. The root is where both paths install. It is deliberately not the release
134
+ package's own directory, which a self-update replaces whole. Under it sit the pinned Node, each
135
+ family's programs, and `workspaces/<family>`, the directory that family's harness runs in — one
136
+ create at install covers all three, and a worker enrolled on somebody's own machine can take no
137
+ second one.
138
+ - **`worker enroll`** redeems the enrollment token this machine was given, prints the row it was
139
+ issued, and then serves that machine. It does not stop at the redemption, because the holder key
140
+ the redemption bound lives in this process.
141
+ - **`worker check [--harness <families>] [--root <dir>]`** reads the runtime image contract from
142
+ inside an image, which is where the rest of that contract cannot be read. An inspection from the
143
+ container runtime sees the command, the account it starts as, the platform it was built for and the
144
+ version label; whether a family's programs are there at the versions this build's facts pin, whether
145
+ the Orchestra CLI is on the path that family's harness is handed, whether a Linux image has the `agent`
146
+ account at uid 1000 with home `/home` and glibc's loader, and whether a Windows image has
147
+ `WinDivert.dll` and `WinDivert64.sys` under `<root>\bin`, where the redirect loads them, are only
148
+ visible from within. Each miss names the line of
149
+ `agents/runtime/image/Dockerfile` that fixes it, and every miss is reported in one run, because the
150
+ rows are independent and a person rebuilding an image should read the whole list once.
151
+
152
+ It starts nothing, claims no identity and speaks to no platform, so it needs neither a bootstrap nor
153
+ a daemon: `docker run --rm --entrypoint worker <image> check --harness <family>` is the whole of how
154
+ it is used. With no `--harness` it checks the rows that belong to no family and says so — a customer
155
+ image installs the families it needs, so holding one to every family this build can host would
156
+ refuse it for lacking programs nothing on it was ever going to run.
157
+ - **`worker trust-nested [--registry <host>]... [--root <dir>]`** lets the containers this machine
158
+ runs trust the egress edge, which nothing does by default. It reads the authority from
159
+ `SFDK_EGRESS_CA_FILE`, which the worker sets on every harness process and on the setup script;
160
+ writes it into dockerd's `certs.d` for the well-known registries and each one named; writes
161
+ `k3d-registries.yaml` under the root, whose `ca_file` is the authority's path inside each k3d node;
162
+ and prints the flags `docker run`, compose and `k3d cluster create` need. On a machine that names no
163
+ authority, such as one with no edge in front of it, it says so, writes nothing and succeeds.
164
+ - **`worker version`** prints this binary's version, the release package it belongs to, and the
165
+ protocol versions it speaks.
166
+
167
+ The version is stamped at link time by the publishing lane, and both `package.json` files carry
168
+ `0.0.0` until that lane stamps them; a build that stamps nothing says so rather than claiming a
169
+ version it is not.
170
+
171
+ ## The bootstrap a machine is started with
172
+
173
+ Everything above assumes the worker already knows where its platform is. That is the one family of
174
+ names it reads before it can speak to anything, so they are the worker's own rather than the
175
+ contract's: the environment document owns the other `SFDK_` names the platform or the worker sets for a
176
+ running machine and says these are outside its scope. They carry the same reserved prefix, so an environment
177
+ cannot declare one; the only `SFDK_` names an environment may declare are the release settings
178
+ `fwp/schema/environment-v1.json` lists under `customerDeclarable`, which the worker reads from its own
179
+ environment in preference to the register answer. The platform composes what is listed here; a value it does not
180
+ set is a concern this machine does not have.
181
+
182
+ | name | what it carries |
183
+ |---|---|
184
+ | `SFDK_CLAIM_ENDPOINT_URL` | the platform's origin, not a route: the paths belong to the protocol |
185
+ | `SFDK_TRANSPORT_TRUST_ROOTS` | the certificate bundle this machine trusts, inline |
186
+ | `SFDK_TRANSPORT_TRUST_ROOTS_FILE` | the same bundle as an absolute path, for a delivery that cannot carry the newlines; setting both is refused |
187
+ | `SFDK_CLAIM_LOCATOR` | the machine row this worker claims as |
188
+ | `SFDK_ENROLLMENT_TOKEN` | the show-once token an enrolling machine redeems; mutually exclusive with the locator |
189
+ | `SFDK_ENROLLMENT_TOKEN_FILE` | the same token as an absolute path; setting both is refused |
190
+ | `SFDK_HOLDER_KEY_ALGORITHM` | which of the protocol's two holder-key algorithms this registration declares |
191
+ | `SFDK_SUBSTRATE` | which evidence this worker gathers, so an unknown value is refused rather than defaulted |
192
+ | `SFDK_CLAIM_AUDIENCE` | who the presented caller-identity request was meant for; required on the virtual-machine lanes |
193
+ | `SFDK_STS_ENDPOINT_URL` | the security-token endpoint the platform will send that request to |
194
+ | `SFDK_SERVICE_ACCOUNT_TOKEN_FILE` | where the audience-bound projected token is mounted, on the Kubernetes lane |
195
+ | `SFDK_TPM_DEVICE_PATH` | the chip's device path where it is not this operating system's default |
196
+ | `SFDK_SUBSTRATE_TOKEN` | the `local` substrate's stand-in factor, inline |
197
+ | `SFDK_SUBSTRATE_TOKEN_FILE` | the same factor as an absolute path; setting both is refused |
198
+ | `SFDK_INTERCEPT_PORTS` | the destination ports whose traffic goes to the egress edge, comma separated; naming none installs no redirect |
199
+ | `SFDK_INTERCEPT_BYPASS` | the shared services the worker's own uid reaches directly, as `[{"host": …, "port": …}]` — our broker and our gateway, and nothing broader |
200
+ | `SFDK_INTERCEPT_SKIP_RANGES` | the private ranges the redirect never takes, for any uid on any port, as comma-separated CIDR ranges; a range that is not wholly private is refused |
201
+ | `SFDK_EDGE_ENDPOINT_URL` | where the redirect sends this machine's traffic; its scheme is the whole TLS statement for that hop |
202
+ | `SFDK_EGRESS_LISTEN_PORT` | where the redirect lands, which is the shim's own listen port |
203
+ | `SFDK_EGRESS_IPV6` | `true` mirrors the redirect through `ip6tables` |
204
+ | `SFDK_EGRESS_PROXY` | `on`, the default, or `off`; `off` tells each harness that no edge of ours stands in front of this machine, as on an enrolled one. The platform writes `off` into every Launcher machine's boot file, because no proxy entrance of ours stands in front of one yet |
205
+ | `SFDK_RUNTIME_ENVIRONMENT` | a whole environment folded into one JSON object of string values, for a boot file whose lines cannot carry a line break; each pair is placed unless already set |
206
+ | `SFDK_VOLUME_MOUNTS` | the volumes this launch declares, as one JSON document |
207
+ | `SFDK_MACHINE_SLUG` | the machine's platform-minted slug: the leading label of its preview hostnames, and what the tunnel's upgrade signs |
208
+ | `SFDK_TUNNEL_ENDPOINT_URL` | where the preview gateway serves its tunnel listener; its scheme is the whole TLS statement for that hop |
209
+ | `SFDK_INGRESS_PORTS` | the loopback ports this machine projects through that tunnel, comma separated; with no reserved ports below, naming none holds no tunnel |
210
+ | `SFDK_INGRESS_ANY_PORT_EXCEPT` | the reserved loopback ports, comma separated; set, the tunnel is held from start, forwards every other port whatever `SFDK_INGRESS_PORTS` says, and the worker registers `ingress-any-port` |
211
+
212
+ **The worker reads that file itself, on every operating system, and no service manager is handed it.**
213
+ Every `worker run` places each pair into its own environment before it serves, leaving alone any name
214
+ that is already set, so a person's own foreground run still wins over the file. systemd's
215
+ `EnvironmentFile=` is deliberately not used: it processes backslash escapes in an unquoted value, so
216
+ the two characters `\n` that carry a line break inside `SFDK_RUNTIME_ENVIRONMENT` would arrive as a bare
217
+ `n` and the certificate folded in there would hold no certificate at all. A machine with no file
218
+ starts anyway — that is an enrolled machine's ordinary start — and a line that is not `NAME=VALUE`
219
+ stops the start and is named.
220
+
221
+ Where the file sits is the one thing that differs, and only because of who may write it. On a Linux
222
+ machine the platform launched, cloud-init writes `/etc/sfdk/machine.env` from the user data and the run
223
+ reads it; no install by a person writes there. A `worker` install that registers a service writes
224
+ `<root>/machine.env` under the worker's own root — `C:\ProgramData\sfdk\worker\machine.env` on a default
225
+ machine-wide Windows install — because an install that registers the worker in one person's own session
226
+ cannot write under `/etc`. A Linux run reads both, the machine-wide one first. The platform's user data writes that file on a machine
227
+ it launched, and a person enrolling one writes it or exports the same names in the shell that runs
228
+ `worker enroll`.
229
+
230
+ Beside them the worker keeps one file of its own, `<root>/state/machine-identity.json`, owner-only.
231
+ It records the row this worker is bound to and — on a machine with no chip — the software holder key
232
+ it is bound under, and it decides whether the next start claims or reattaches. A row this
233
+ worker has already claimed cannot be claimed again, so a start that finds its own record takes the
234
+ next generation under the key instead, and it treats the enrollment token its bootstrap file still
235
+ carries as spent rather than refusing to start beside the row it names. The record also carries the
236
+ last run that registered with the platform and the epoch it registered under, which is what an
237
+ install waits for: everything else in it is written at the claim, before there is a broker link at
238
+ all. It is kept only where the row outlives the program: a virtual
239
+ machine that parks and wakes with its own disk, and a machine a person enrolled. A pod is replaced
240
+ rather than woken, and its row is fresh with it.
241
+
242
+ ## The in-guest egress
243
+
244
+ On the virtual-machine lanes this worker's own process is the transparent proxy, and it puts three
245
+ things in place in one order that is not interchangeable:
246
+
247
+ 1. **The edge's certificate authority into this machine's trust store**, from the file the launch
248
+ wrote (or, where that file is not there, from the URL it names). Windows reads the machine
249
+ certificate stores and nothing else, so a certificate that is not there is every TLS client on
250
+ the machine failing its handshake with the edge; and on Windows a chain verdict computed before
251
+ the import stays cached machine-wide, so an import that actually wrote also throws that cache
252
+ away. It happens first because the redirect is live the moment it lands.
253
+ 2. **The listener**, bound before any rule exists, so no connection is ever sent to a port with
254
+ nothing behind it. On Windows an inbound firewall rule is written first as well: the rewritten
255
+ packet arrives as an inbound connection, which Windows Server blocks by default.
256
+ 3. **The redirect** — `iptables` on Ubuntu, WinDivert on Windows. The two WinDivert files are loaded
257
+ by absolute path from `<root>\bin`, where the image bake stages them, and never through the
258
+ operating system's own search order: opening the driver installs and starts a kernel service.
259
+
260
+ Each accepted connection's original destination is recovered (`SO_ORIGINAL_DST` on Linux, the
261
+ redirect's own proxy-port map on Windows; a recovery that misses is refused rather than sent to the
262
+ peer, which carries the right host and the wrong port) and carried to the edge under the machine
263
+ token this worker holds. There is no loopback round trip on these lanes: the process that holds the
264
+ token is the process that presents it.
265
+
266
+ What the worker's own uid reaches **without** the redirect is the launch's list — our broker and our
267
+ gateway — plus three the launch cannot state: the egress edge, because redirecting the hop that
268
+ carries a redirected connection is a loop with no exit; the platform's identity endpoint, because
269
+ this worker calls it before it holds the token the edge would identify it by; and the metadata
270
+ service, which answers the evidence that call presents. Nothing is uid-wide, so the object store
271
+ stays intercepted and this worker's own content pulls meet the platform's rule at the edge.
272
+
273
+ The redirect takes its Windows form on Windows and its `iptables` form everywhere else, whatever the
274
+ substrate. A pod with an egress sidecar runs only the certificate step: the sidecar installs that
275
+ lane's redirect and runs its own shim, which reads the token from the loopback feed below. A pod with
276
+ no sidecar, as a customer's Launcher creates, names an intercept set and this worker installs the
277
+ redirect itself.
278
+
279
+ The private ranges `SFDK_INTERCEPT_SKIP_RANGES` names are never redirected, for any uid on any port:
280
+ the rules return them ahead of the redirect, a nested container reaches them through its bridge, and
281
+ the Windows filter excludes each range beside the single destinations.
282
+
283
+ The proxy names a customer declares (`HTTPS_PROXY`, `HTTP_PROXY` and `NO_PROXY`, in either case) serve
284
+ this worker's own calls to the platform and the broker. `NO_PROXY` must then hold `169.254.169.254`,
285
+ because the metadata service is never reached through a proxy. Where a redirect is in place, the same
286
+ names are left out of the environment each harness and the setup script are handed, so the agent's
287
+ calls reach the redirect, which carries them to the edge.
288
+
289
+ ## The egress shim's token feed
290
+
291
+ The shim that intercepts this machine's traffic stamps the machine token on it, and the worker is
292
+ what holds that token. On Kubernetes the two are in different containers of one pod, so the only
293
+ channel between them is the network: the worker listens on `127.0.0.1:7788` — a constant both halves
294
+ carry, not a placed value — and the shim connects with retry.
295
+
296
+ Each frame is one JSON line, newline-terminated:
297
+
298
+ ```
299
+ {"generation": 7, "token": "<the machine token>", "expiresAt": "2026-09-12T10:15:00.123456Z"}
300
+ ```
301
+
302
+ On connect the worker writes the generation this machine holds now, and one line per rotation after
303
+ that. It writes nothing else on that socket and never reads from it, and it listens on loopback and
304
+ nowhere else. A worker restart drops the connection; the shim reconnects and is fed the current
305
+ generation again. The broker credential co-issued beside the token never crosses this channel, and
306
+ neither does the holder key.
307
+
308
+ ## Building and testing
309
+
310
+ ```
311
+ make verify # gofmt, then vet for linux, windows and macOS
312
+ make vet-all # the vet half on its own, which is what type-checks the per-operating-system files
313
+ make test # the script harness's own tests, then go test with the coverage gate
314
+ make build # one static binary per release package, into npm/<platform>/, with the script harness beside it
315
+ ```
316
+
317
+ ### What a release is
318
+
319
+ `make build` writes the five per-platform binaries into their package directories and copies the
320
+ `script` harness in beside each, and those directories are what the publishing workflow packs. Two
321
+ shapes of tarball come out of it, and the layout is stated here because that workflow reads it
322
+ rather than deciding it:
323
+
324
+ ```
325
+ @orchestraworks/worker-<platform> @orchestraworks/worker
326
+ package.json package.json
327
+ worker (worker.exe on win32) bin/worker.js
328
+ script-harness/ README.md
329
+ package.json LICENSE
330
+ index.js
331
+ fetch-failure.js
332
+ LICENSE
333
+ ```
334
+
335
+ Six published packages redistribute this tree, and an Apache-2.0 package that ships without the licence
336
+ is redistributed on terms it does not carry — so `LICENSE` is one file here and a copy in every tarball.
337
+
338
+ The per-platform tarball is the release artifact: its SHA-256 is the one digest the catalog carries,
339
+ the register answer names and both fetch paths check, and it covers the binary and the `script`
340
+ harness together, so a self-update carries the program with the binary. The launcher package holds
341
+ no binary at all — it depends on the five as optional dependencies and runs whichever one npm
342
+ installed for this machine. The build's output is ignored by `.gitignore`: the packages are
343
+ published from a build rather than from the tree.
344
+
345
+ The tests are Go tests plus two sets of Node's own — the `script` harness's, and the npm launcher's: no container runtime, no
346
+ database, no stack, and no harness family is ever downloaded — a fake registry serves small archives
347
+ that prove the install's own rules. The tests that
348
+ need a real trusted platform module skip where there is none, and the broker suite drives a real
349
+ `nats-server` named by `WORKER_TEST_NATS_SERVER` and skips when that is unset — so a `make test`
350
+ meant to meet the coverage gate sets it:
351
+
352
+ ```
353
+ WORKER_TEST_NATS_SERVER=/path/to/nats-server-2.14.6 make test
354
+ ```
355
+
356
+ `WORKER_TEST_NATS_SERVER` is a knob a test process sets for itself, not a variable the platform sets
357
+ on a machine, which is why it carries no `SFDK_` prefix and appears in no environment document.
358
+
359
+ ## Where a family keeps its state
360
+
361
+ Every family keeps its conversation history — the transcripts that let a later session pick up where an
362
+ earlier one stopped — in a directory it derives from `$HOME`. That is true of all four, and nothing in the
363
+ runtime image baits any of them elsewhere:
364
+
365
+ | family | state directory | how it resolves |
366
+ | --- | --- | --- |
367
+ | claude | `$HOME/.claude/` | Node `os.homedir()` — `$HOME` first, `getpwuid` fallback. |
368
+ | codex | `$HOME/.codex/` | The Rust `home` crate, used when `CODEX_HOME` is unset. |
369
+ | pi | `$HOME/.pi/agent/` (transcripts under `sessions/`) | `process.env.PI_CODING_AGENT_DIR ? resolve(...) : join(homedir(), ".pi", "agent")` — `pi-acp@0.0.33` `dist/index.js:1381` and `:1725`, and the harness's own `getAgentDir()` at `@earendil-works/pi-coding-agent@0.84.4` `dist/config.js:420-426`, whose variable NAME is assembled at `:405` from `APP_NAME` (`"pi"`). Transcripts: harness `dist/config.js:456-457`, adapter `dist/index.js:1397-1399` — the adapter prefers a `sessionDir` key in `<agentDir>/settings.json` when one is set (`:1383-1396`), and the baked settings file sets none. `pi-acp` also keeps its own session map at `$HOME/.pi/pi-acp/` (`:352`). |
370
+ | script | none | The harness holds no conversational state. |
371
+
372
+ An environment makes that state durable by declaring a volume mount at `/home`, which is where the
373
+ account inside the runtime image already has its home directory. An environment that declares no such
374
+ mount behaves exactly as it did before: the state is written to the machine's own disk and goes with it.
375
+
376
+ That is why no image bakes `CODEX_HOME` or `PI_CODING_AGENT_DIR`. A pointer to a path outside `$HOME` is
377
+ exactly what a durable home cannot survive: it would send codex's and pi's transcripts to the container's
378
+ writable layer, which a recycle discards. Leaving both knobs unset lets each family's own `$HOME` fallback
379
+ engage. Injecting a literal `CODEX_HOME="$HOME/.codex"` instead was rejected because env-value expansion is
380
+ not portable — Kubernetes expands only `$(VAR)` and Docker expands nothing.
381
+
382
+ ### First-boot seeding
383
+
384
+ codex and pi each need one file to already exist inside their state directory: codex's egress-placeholder
385
+ `auth.json`, pi's provider-pinning `settings.json`. Both are image content, but the directory they belong
386
+ in lives on a volume that is empty the first time a machine boots. So the image bakes each file as a
387
+ template and the family's `seed` list in `facts/families.json` says where it goes:
388
+
389
+ ```
390
+ codex: /opt/software-factory-codex/auth.json -> .codex/auth.json
391
+ pi: /opt/software-factory-pi/settings.json -> .pi/agent/settings.json
392
+ ```
393
+
394
+ The worker copies each pair before it spawns the harness, resolving every destination against `$HOME`. The
395
+ copy is **copy-if-absent**: the first placement lays the templates down, and every later one finds whatever
396
+ the harness has written since — a refreshed token, an edited setting — and leaves it alone. Seeded files are
397
+ created `0600`, parent directories `0700`, and the step never creates `$HOME` itself. The claude and script
398
+ families declare an empty `seed`, so the step is a no-op for them.
399
+
400
+ ### Whether a conversation resumes
401
+
402
+ Durable storage makes state survive a recycle; whether a family *resumes* a conversation also needs its ACP
403
+ adapter to implement `session/load`. All three model-backed adapters do, verified against the shipped
404
+ artifacts:
405
+
406
+ - **`claude-agent-acp@0.70.0`** — advertises `agentCapabilities.loadSession: true` and implements
407
+ `loadSession(params)`, registered on the connection as the `session/load` request handler
408
+ (`dist/acp-agent.js:788`, `:6909`).
409
+ - **`codex-acp@1.11.0`** — implements `session/load` by resuming the codex thread and replaying that
410
+ thread's history as `session/update` notifications before it answers the request. Read in the adapter's
411
+ built code at the pinned version; the `0.16.0` adapter it replaced carried the same method inside its own
412
+ native binary.
413
+ - **`pi-acp@0.0.33`** — advertises `agentCapabilities.loadSession: true` and implements `loadSession(params)`,
414
+ which respawns pi against the stored session file and replays its messages (`dist/index.js:1935`, `:2460`;
415
+ the helper that binds a session object to an existing pi process is at `:753`). Confirmed on the wire: an
416
+ ACP `initialize` against the shipped binary answers `protocolVersion: 1` with
417
+ `agentCapabilities.loadSession: true`. That is the *capability*, not the replay itself — pi still has no
418
+ captured `session/load` transcript, which is the one resume-evidence gap open on this lane.
419
+
420
+ ## The command's environment is the family's pass-through list, and nothing else
421
+
422
+ The worker does not *merge* an environment into a harness process: the family's `passEnv` enumeration in
423
+ `facts/families.json` **is** that process's complete environment. An environment cannot declare an
424
+ `SFDK_` name for it, because `SFDK_` is reserved apart from the four `SFDK_WORKER_*` release settings, which
425
+ reach the worker and never a harness process. A derived image that bakes a name the list does not carry, such as `ENV JOB_TARGET=…`, gets **silence**,
426
+ not an override: the variable never reaches the command, and a job running under `set -u` aborts on the
427
+ first reference to it. Per-job configuration travels the prompt, the agent's instructions, or a file in the
428
+ image.
429
+
430
+ Every family's harness process also gets `SFDK_SESSION_ID`, which the worker sets from the `sessionId` on
431
+ `session_placement.apply`: the worker adds it itself, so it needs no `passEnv` entry, whatever the worker's
432
+ own environment holds. The `script` family adds only
433
+ `SFDK_ORGANIZATION_ID` to the command it runs, from its stored conversation metadata; its command inherits
434
+ the session variable from the harness process. Neither grants egress authority.
435
+
436
+ **On Windows that list is shorter than the operating system's own habits assume.** It is
437
+ OS-independent, so a Windows job sees `PATH`, `HOME` and `USERPROFILE` and *not* `TEMP`, `APPDATA` or
438
+ `ComSpec` — names a PowerShell one-liner reaches for without thinking (`$env:TEMP`). The list is not a
439
+ filter over the environment: the worker *builds* the child's environment from it, so a name the platform
440
+ set on the service and the family does not declare is simply gone by the time the command runs.
441
+
442
+ **Three names are backfilled for Windows and none of them is on any family's list.** A backfill is the
443
+ treatment for operating-system *plumbing* — a variable the machine needs to work at all, as opposed to
444
+ configuration a customer chose — and it is if-absent, so a declared spelling always wins. Two come from
445
+ libraries: Go's `os/exec` adds `SYSTEMROOT` whenever a caller sets an explicit environment, and libuv copies
446
+ its own required-variable set into a spawned child from *this* process's environment — which is why `PATH`
447
+ reaching the command is what lets `powershell.exe` and `taskkill.exe` resolve at all. The third is ours:
448
+ **`PATHEXT`**, the list of extensions PowerShell treats as *executable*. With it unset the effective list
449
+ collapses to `.CPL`, so a `curl.exe` PowerShell resolved on `PATH` is a **document** rather than a program
450
+ and invoking it throws `CantActivateDocumentInPipeline` — before a process starts, before a packet leaves.
451
+ Every network probe on the Windows lane came back `000`, which reads exactly like a blocked connection.
452
+
453
+ The neighbours a reader expects beside it were **measured and left out**, each changing nothing on a Windows
454
+ Server 2025 runtime AMI: `cmd.exe` and `.bat` files run from CreateProcess's own default without `ComSpec`,
455
+ `%SystemRoot%` expands from the `SYSTEMROOT` `os/exec` already backfills, `SystemDrive` and `windir` are
456
+ unused by realistic work, and a service's temp directory falls back to `C:\Windows\SystemTemp` without
457
+ `TEMP` or `TMP`. Add a fourth name only with that kind of evidence. Machine plumbing is not configuration
458
+ and must never be pushed into the prompt: a job cannot set `PATHEXT` for a shell that has already refused to
459
+ start its first program.
460
+
461
+ Three more bounds a job author should know:
462
+
463
+ - **Turn wall clock.** The platform bounds a turn with its own response timeout (default 600 seconds,
464
+ configurable). A job that can run longer needs that raised, or it is killed mid-turn and reported as a
465
+ runtime failure regardless of what the script was doing.
466
+ - **`session/load` re-executes.** The script harness holds no conversational state, so a resumed
467
+ conversation's command runs again. That is the safe direction — a fresh, deterministic run — and another
468
+ reason to keep commands idempotent.
469
+ - **`cwd` is the session's.** `session/new` (and `session/load` on a revival) carries the workspace path, the
470
+ worker maps it to this machine's native filesystem — `C:\sf\workspace` on Windows — and the harness spawns
471
+ the command there. A session that declares none inherits the harness's own directory; one that does not
472
+ exist fails the spawn, which the turn reports as an ordinary exit-127 failure with the operating system's
473
+ message attached.
474
+
475
+ ## What the `script` harness answers
476
+
477
+ The worker runs one harness process per session placement and speaks ACP to it as its client. The `script`
478
+ family's harness is the one the worker carries itself (`script-harness/`), and its dispatch table is:
479
+
480
+ - `initialize` — the harness identifies itself.
481
+ - `session/new` — a fresh session id, with the session's declared working directory anchored on it.
482
+ - `session/load` — the session's replay, as the `session/update` notifications emitted between the request
483
+ and its response. It executes nothing and calls nothing, and it restores the duplicate-execution guard
484
+ below, which the notifications cannot carry.
485
+ - `session/prompt` — execute the agent's instructions, stream stdout and stderr as `session/update`
486
+ notifications, move platform state from the exit code, then answer. Streaming is bounded twice: each frame
487
+ is sliced at 32000 characters (one over-long line would trip the line scanner and kill every session on
488
+ the pipe, not just this turn), and a turn's **total** streamed volume is capped at 5 MiB of **wire** bytes
489
+ — the JSON-encoded frame text, since that is what the platform records, and JSON escaping inflates
490
+ control-heavy output up to about six times (one NUL byte is six wire bytes). Past the ceiling the harness
491
+ emits one truncation notice and stops streaming, while the command still runs to completion with its
492
+ exit-code contract unchanged.
493
+ - `session/cancel` — stop the in-flight command **and everything it started**, then answer. On Linux that is
494
+ a `SIGTERM` to the command's whole **process group** (commands run detached, so the group dies with the
495
+ command and not just with the `bash -c` parent); on Windows there are no process groups to signal, so it
496
+ is `taskkill /T /F /PID <pid>`, which walks the process tree. A cancelled turn made no determination, so
497
+ it claims nothing and answers the prompt with a JSON-RPC error. Whether a turn counts as cancelled is read
498
+ off an observed **fact about the kill**, never off the arrival of the cancel, and each operating system
499
+ supplies the fact it can: on Linux the command's **exit shape** (one the signal actually killed reports
500
+ `signal = SIGTERM`, one that had already exited reports its own status), and on Windows — which terminates
501
+ a process with an exit *code* and no signal — the kill's own answer, so the harness checks whether the
502
+ shell was still running when the cancel arrived and only then kills. Either way a cancel racing a script's
503
+ own `exit 0`, including the window after the shell exits but before the harness reaps it, leaves the turn
504
+ with the result it earned, claim included. (That liveness check is also a safety requirement on Windows,
505
+ which recycles process ids: a `/T` on a stale one would kill a stranger's process tree.)
506
+
507
+ A turn that settles while a backgrounded grandchild still holds the pipes signals that process group and
508
+ detaches the streams, so nothing leaks into the next turn's transcript. That sweep is **conditional and
509
+ best-effort**, and a job author should not read it as a guarantee that background work dies with the turn:
510
+ it runs only on the grace-timer path, so a background job whose stdio is redirected (`cmd >/dev/null 2>&1 &`)
511
+ lets the pipes close promptly, settles the turn before the timer, and survives it untouched. It is also
512
+ `SIGTERM`-only — a process that traps or ignores `TERM` survives either way. Nothing of the sort leaks into
513
+ the *transcript*; what survives is the process.
514
+
515
+ A second prompt on a **workflow-step** session whose earlier claim is confirmed landed is answered as a
516
+ no-op turn without re-executing the command (warm reuse and steer promotion both make that routine); an
517
+ earlier claim that is merely *attempted* is re-confirmed against the run first. The no-op is step-scoped on
518
+ purpose: an ordinary message appended to a direct-start session is a real new turn and always executes.
package/bin/worker.js ADDED
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ // The launcher npm puts on the path. It runs the binary out of the per-platform package npm installed
3
+ // for this machine and stays in front of it for the process's whole life, because a service manager
4
+ // supervises the command it started: the pod's SIGTERM lands on this process, and the worker's shutdown
5
+ // report and orderly harness stop only happen if it reaches the binary behind it.
6
+
7
+ import { spawn } from "node:child_process";
8
+ import { createRequire } from "node:module";
9
+ import { constants } from "node:os";
10
+ import { existsSync } from "node:fs";
11
+
12
+ const require = createRequire(import.meta.url);
13
+
14
+ const OPERATING_SYSTEMS = { linux: "linux", darwin: "darwin", win32: "win32" };
15
+ const ARCHITECTURES = { x64: "x64", arm64: "arm64" };
16
+
17
+ // The three a supervisor ends a process with. Everything else is either unforwardable (SIGKILL, SIGSTOP)
18
+ // or something the worker has no shutdown meaning for.
19
+ const FORWARDED_SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"];
20
+
21
+ function binaryPath() {
22
+ const operatingSystem = OPERATING_SYSTEMS[process.platform];
23
+ const architecture = ARCHITECTURES[process.arch];
24
+ if (!operatingSystem || !architecture) {
25
+ throw new Error(`there is no worker release for ${process.platform}/${process.arch}`);
26
+ }
27
+
28
+ const packageName = `@orchestraworks/worker-${operatingSystem}-${architecture}`;
29
+ const binary = operatingSystem === "win32" ? "worker.exe" : "worker";
30
+ let resolved;
31
+ try {
32
+ resolved = require.resolve(`${packageName}/${binary}`);
33
+ } catch {
34
+ // The per-platform packages are optional dependencies, so an install that skipped this one — a
35
+ // lockfile from another machine, or `--no-optional` — leaves the launcher with nothing to run.
36
+ throw new Error(
37
+ `${packageName} is not installed; install it, or reinstall @orchestraworks/worker on this machine`,
38
+ );
39
+ }
40
+ if (!existsSync(resolved)) {
41
+ throw new Error(`${packageName} carries no ${binary}`);
42
+ }
43
+ return resolved;
44
+ }
45
+
46
+ // A signal is not an exit status: reported as the shell convention so a supervisor reading the status
47
+ // can tell a killed worker from one that exited.
48
+ function statusFor(code, signal) {
49
+ if (!signal) {
50
+ return code ?? 1;
51
+ }
52
+ return 128 + (constants.signals[signal] ?? 0);
53
+ }
54
+
55
+ function run(binary) {
56
+ const child = spawn(binary, process.argv.slice(2), { stdio: "inherit" });
57
+
58
+ for (const signal of FORWARDED_SIGNALS) {
59
+ // Listening at all is what stops Node ending the launcher on its own and orphaning the worker
60
+ // mid-shutdown: the launcher outlives the signal and exits on the child's exit instead.
61
+ process.on(signal, () => {
62
+ if (child.exitCode === null && child.signalCode === null) {
63
+ child.kill(signal);
64
+ }
65
+ });
66
+ }
67
+
68
+ child.on("error", (failure) => {
69
+ console.error(`worker: ${failure.message}`);
70
+ process.exit(1);
71
+ });
72
+ child.on("exit", (code, signal) => process.exit(statusFor(code, signal)));
73
+ }
74
+
75
+ try {
76
+ run(binaryPath());
77
+ } catch (failure) {
78
+ console.error(`worker: ${failure.message}`);
79
+ process.exit(1);
80
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@orchestraworks/worker",
3
+ "version": "0.0.134",
4
+ "type": "module",
5
+ "description": "The Software Factory worker: the one program that runs inside a machine and speaks the worker protocol to the platform.",
6
+ "license": "Apache-2.0",
7
+ "author": "Living Computers",
8
+ "homepage": "https://orchestraworks.ai",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/livingcomputers/software-factory.git"
12
+ },
13
+ "engines": {
14
+ "node": ">=22"
15
+ },
16
+ "bin": {
17
+ "worker": "bin/worker.js"
18
+ },
19
+ "files": [
20
+ "bin",
21
+ "README.md"
22
+ ],
23
+ "optionalDependencies": {
24
+ "@orchestraworks/worker-linux-x64": "0.0.134",
25
+ "@orchestraworks/worker-linux-arm64": "0.0.134",
26
+ "@orchestraworks/worker-darwin-x64": "0.0.134",
27
+ "@orchestraworks/worker-darwin-arm64": "0.0.134",
28
+ "@orchestraworks/worker-win32-x64": "0.0.134"
29
+ }
30
+ }