threadwire 0.1.6 → 0.1.8
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 +40 -0
- package/README.md +13 -2
- package/TELEGRAM-INGRESS.md +8 -2
- package/bin/isolated-runtime.js +5 -0
- package/bin/model-broker.js +5 -0
- package/docs/isolated-provider-runtime.md +137 -0
- package/package.json +5 -1
- package/scripts/provider-shims/front-door.sh.template +18 -0
- package/scripts/verify-package.js +15 -0
- package/src/absolute-deadline.js +94 -0
- package/src/cli.js +120 -21
- package/src/delegated-result-admission.js +1 -1
- package/src/docker-api.js +131 -0
- package/src/isolated-runtime-client.js +149 -0
- package/src/isolated-runtime.js +982 -0
- package/src/isolated-state.js +409 -0
- package/src/isolated-worker.js +123 -0
- package/src/model-broker-policy.js +139 -0
- package/src/model-broker.js +313 -0
- package/src/mount-policy.js +28 -0
- package/src/normalized-output.js +68 -0
- package/src/relay-write.js +44 -0
- package/src/telegram-ingress/config.js +7 -0
- package/src/telegram-ingress/core.js +40 -0
- package/src/telegram-webhook.js +6 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.1.8 - 2026-07-26
|
|
6
|
+
|
|
7
|
+
- Add `threadwire run --transcript <path>` to durably capture the exact normalized
|
|
8
|
+
stdout JSONL protocol in a private, exclusive file while keeping activity logs
|
|
9
|
+
and scoped raw evidence artifacts separate.
|
|
10
|
+
- Add an opt-in, fail-closed isolated Codex write runtime. A root-owned
|
|
11
|
+
supervisor launches one worktree-only worker per task while a separate narrow
|
|
12
|
+
model broker retains upstream credentials. Fresh/resume, secret rotation,
|
|
13
|
+
filesystem/network/socket escapes, configuration overrides and cleanup are
|
|
14
|
+
covered by adversarial Docker E2E.
|
|
15
|
+
- Bind broker grants to dedicated trusted run-network listeners and an exact
|
|
16
|
+
allowlisted model/request schema; add stable worktree ancestry checks,
|
|
17
|
+
task-private persistent resume state, contained nested cwd and structured
|
|
18
|
+
Codex arguments. The Compose deployment is now an explicit opt-in override.
|
|
19
|
+
- Allocate authenticated, uniquely labeled state per fresh Codex lineage;
|
|
20
|
+
reject volume adoption and nested worktree mounts; use Docker's structured
|
|
21
|
+
host-bind API; and require an explicit supervisor-side model allowlist.
|
|
22
|
+
- Bound and eagerly expire preflight capabilities, serialize writable lineage
|
|
23
|
+
resumes, and automatically reconcile and garbage-collect authenticated state
|
|
24
|
+
volumes with restart-safe cleanup retry.
|
|
25
|
+
- Bound active runs and Docker/broker operations through cleanup, reserve state
|
|
26
|
+
before awaited revalidation, autonomously close expired grant listeners, and
|
|
27
|
+
reconcile exactly owned workers and networks across every container state.
|
|
28
|
+
- Abort accepted broker work and caller-disconnected runs at one absolute
|
|
29
|
+
deadline; make crash cleanup authority an fsync'd authenticated full run
|
|
30
|
+
identity rather than labels; and durably fsync registry rename evidence.
|
|
31
|
+
- Carry one launch deadline from preflight through prompt/secret/evidence/
|
|
32
|
+
Telegram setup, worker execution and cleanup; make supervisor and broker
|
|
33
|
+
shutdown abort control uploads and active work; move Docker GC outside the
|
|
34
|
+
registry transaction; and stream-cap broker/runtime responses.
|
|
35
|
+
- Gate worker start on one-shot activation of a verified pending broker grant
|
|
36
|
+
and address its listener by the inspected per-run IP, removing dynamic
|
|
37
|
+
network-alias readiness races.
|
|
38
|
+
- Route relay-profile Telegram Codex jobs exclusively through the isolated
|
|
39
|
+
runtime, keep validate-only launches local, preserve bounded stdout/stderr
|
|
40
|
+
evidence channels, and protect live lineages from concurrent reconciliation.
|
package/README.md
CHANGED
|
@@ -7,6 +7,13 @@ non-root Codex provider, Docker secrets, loopback TLS proxy, verification, and
|
|
|
7
7
|
rollback path, see [Container runtime](docs/container-runtime.md). The legacy
|
|
8
8
|
host launcher below remains supported and is separate from that deployment.
|
|
9
9
|
|
|
10
|
+
Relay-originated Codex writes are explicit and default off. `--relay-write`
|
|
11
|
+
requires a reviewed `--workspace-profile` plus the root-owned isolated runtime
|
|
12
|
+
and credential broker. The worker gets one writable task worktree, no upstream
|
|
13
|
+
or Git credential, and no route except its run-scoped broker. Missing
|
|
14
|
+
prerequisites fail before prompt or credential reads with no native fallback.
|
|
15
|
+
See [Isolated provider runtime](docs/isolated-provider-runtime.md).
|
|
16
|
+
|
|
10
17
|
## Run with npx
|
|
11
18
|
|
|
12
19
|
Threadwire requires Node.js 22 or newer. Run the published CLI without a global
|
|
@@ -67,13 +74,17 @@ Threadwire directly spawns the structurally separate terminal adapters `/opt/dat
|
|
|
67
74
|
|
|
68
75
|
The relay contract: the caller supplies `THREADWIRE_TARGET` (`telegram:<chat-id>` or `telegram:<chat-id>:<thread-id>`) and, optionally, `THREADWIRE_PROCESS_NUMBER`. The prompt arrives on a piped stdin; all native provider flags pass through after `--`. A positional prompt is never parsed. A missing `THREADWIRE_TARGET` fails closed (exit code 2) and a malformed one is rejected by the launcher's token-free preflight — the shim never invents a default destination and never silently downgrades a requested relay to a direct run. A shim relays only for a worker mode (`codex exec …`, `opencode-local-fleet run …`, or any non-interactive `claude …`); it drops the worker subcommand before relaying and Threadwire re-adds it around the prompt. Version and help queries, non-worker subcommands, interactive TTY sessions, and any invocation already inside a relayed run (`THREADWIRE_ACTIVE=1`, so nested provider workers stay local/native) run the real CLI directly through the libexec adapter.
|
|
69
76
|
|
|
70
|
-
Pass `--resume-session <exact-provider-session-id>` to continue a stored Codex, Claude, or OpenCode session. Threadwire preserves the provider's native resume ordering and streaming options and never uses implicit latest-session behavior.
|
|
77
|
+
Pass `--resume-session <exact-provider-session-id>` to continue a stored Codex, Claude, or OpenCode session. Threadwire preserves the provider's native resume ordering and streaming options and never uses implicit latest-session behavior.
|
|
78
|
+
|
|
79
|
+
Pass `--transcript <path>` to capture the exact normalized, caller-facing JSONL protocol written to run stdout. The file is created privately (mode `0600`), must not already exist, and contains the same records in the same byte order as stdout. It never contains provider JSONL, reasoning, tool payloads, prompts, stderr, or credentials.
|
|
80
|
+
|
|
81
|
+
`--activity-log <path>` is different: it retains its append contract and writes only minimal safe lifecycle facts—the selected workspace profile and immutable provenance, provider start (`provider`, child `pid`), and available native session ID. It is local-only and never enters Telegram or normalized stdout.
|
|
71
82
|
|
|
72
83
|
Provider stdout is decoded as JSONL with a finite 1 MiB UTF-8 byte limit for each pending unterminated record. Exceeding it rejects the run, removes listeners and signal handlers, and terminates the provider without parsing or rendering the oversized content.
|
|
73
84
|
|
|
74
85
|
For normal `threadwire run` execution, Threadwire stdout is the parent-model admission plane. It emits exactly one versioned terminal `delegated_result` JSON object and no progress records. The envelope contains a bounded, credential-redacted, control-normalized final conclusion when one was produced, structured terminal state and exit code, and the exact bounded provider session handle when available. Its closed schema can also represent concise blocker/decision requests, opaque artifact handles, commit/URL references, and a concise validation summary; current provider adapters do not infer these optional fields from prose. Unknown, nested, wrongly typed, cardinality-invalid, or oversized admission candidates fail closed. See [Delegated-result protocol](docs/delegated-result-protocol.md) for the complete schema, limits, and caller obligations.
|
|
75
86
|
|
|
76
|
-
Set the trusted operator setting `THREADWIRE_EVIDENCE_ROOT` for standalone runs to persist the delegated prompt and raw provider stdout/stderr as a scoped file-backed artifact; Telegram ingress uses its dedicated evidence volume automatically. The parent or owning Telegram topic receives only an opaque bearer handle. Retrieve later with `threadwire evidence read` or the authenticated `/evidence` Telegram command and an explicit bounded selector. See [Evidence artifacts](docs/evidence-artifacts.md) for permissions, quotas, retention, recovery, authorization, redaction, privacy, and cleanup behavior.
|
|
87
|
+
Scoped raw evidence artifacts are separate from both files above. Set the trusted operator setting `THREADWIRE_EVIDENCE_ROOT` for standalone runs to persist the delegated prompt and raw provider stdout/stderr as a scoped file-backed artifact; Telegram ingress uses its dedicated evidence volume automatically. The parent or owning Telegram topic receives only an opaque bearer handle. Retrieve later with `threadwire evidence read` or the authenticated `/evidence` Telegram command and an explicit bounded selector. See [Evidence artifacts](docs/evidence-artifacts.md) for permissions, quotas, retention, recovery, authorization, redaction, privacy, and cleanup behavior.
|
|
77
88
|
|
|
78
89
|
Telegram is the separate user-visible progress plane. Lifecycle notices, diagnostics, tool activity, and assistant streaming continue there independently; `--tool-messages` affects only Telegram. Execution-environment details, provider records and streams, lifecycle progress, diagnostics, tool names/arguments/details/output, stderr, prompts, narration/reasoning, and full transcripts never enter stdout. Threadwire's bounded error diagnostic remains on stderr. Help output is the sole non-run, human-readable stdout mode. This default is an intentional pre-1.0 protocol break from the former mixed Hermes JSONL stream.
|
|
79
90
|
|
package/TELEGRAM-INGRESS.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Telegram Ingress for Threadwire
|
|
2
2
|
|
|
3
|
-
Standalone Threadwire webhook service that accepts Telegram Bot API updates
|
|
3
|
+
Standalone Threadwire webhook service that accepts Telegram Bot API updates.
|
|
4
|
+
Native providers use the existing worker path; when isolated-runtime authority
|
|
5
|
+
is configured, Codex uses isolated preflight/run exclusively with no native
|
|
6
|
+
fallback. Hermes is not involved.
|
|
4
7
|
|
|
5
8
|
## Important bot ownership
|
|
6
9
|
|
|
@@ -49,7 +52,10 @@ The process binds to **loopback (`127.0.0.1`) by default**. Expose it only throu
|
|
|
49
52
|
- Provider child env strips Telegram tokens, webhook secret, allowlists, bind/port, concurrency, tool-message, and update-guard service config. `runWorker` also strips Telegram tokens.
|
|
50
53
|
- No logging of headers, bodies, prompts, or secrets.
|
|
51
54
|
- Webhook secret must be at least 32 characters after trim.
|
|
52
|
-
-
|
|
55
|
+
- Native work returns `202` only after the worker child has actually spawned
|
|
56
|
+
(`onSpawn`). Isolated Codex work returns `202` only after the authenticated
|
|
57
|
+
isolated run completes successfully; preflight alone is never treated as
|
|
58
|
+
dispatch success.
|
|
53
59
|
- Failure before spawn rejects the request with `500` and is reported once via the operational error reporter.
|
|
54
60
|
- Failure after spawn is reported once via the worker-failure reporter (not double-reported through both channels in the real entrypoint wiring).
|
|
55
61
|
- Authenticated irrelevant updates return `200`.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Isolated provider runtime
|
|
2
|
+
|
|
3
|
+
Relay-originated Codex writes are disabled unless the caller supplies both
|
|
4
|
+
`--relay-write` and a reviewed `--workspace-profile`. The write path never
|
|
5
|
+
falls back to the native provider launcher.
|
|
6
|
+
When the Telegram service is configured with isolated-runtime authority, Codex
|
|
7
|
+
webhook jobs use the same preflight/run boundary exclusively; an unavailable
|
|
8
|
+
runtime fails the job rather than invoking the native runner. Raw provider
|
|
9
|
+
stdout and stderr are returned as separately identified, bounded chunks for
|
|
10
|
+
the existing evidence callbacks.
|
|
11
|
+
|
|
12
|
+
## Security boundary
|
|
13
|
+
|
|
14
|
+
Telegram ingress has neither provider secrets nor container-runtime authority.
|
|
15
|
+
The root-owned isolated-runtime supervisor has Docker authority and a read-only
|
|
16
|
+
view of allowlisted worktree roots, but no provider credential. The model
|
|
17
|
+
broker alone mounts the upstream credential. Each grant creates a listener
|
|
18
|
+
bound to the broker's trusted per-run network interface. That listener accepts
|
|
19
|
+
one token, one run/network lineage, one allowlisted model, and a closed
|
|
20
|
+
`/v1/responses` schema; it is not a generic HTTP proxy.
|
|
21
|
+
The listener remains non-authorizing while the supervisor creates and verifies
|
|
22
|
+
the worker. The supervisor then activates it exactly once immediately before
|
|
23
|
+
worker start. Workers receive the inspected per-run broker IP directly, so
|
|
24
|
+
launch does not depend on asynchronous container-DNS alias propagation.
|
|
25
|
+
|
|
26
|
+
For each fresh or explicit-resume run the supervisor creates a unique Docker
|
|
27
|
+
internal network, attaches the broker, and launches an immutable worker. The
|
|
28
|
+
worker has a read-only root, all capabilities dropped, no-new-privileges,
|
|
29
|
+
container-local tmp/run, resource limits, and exactly one writable
|
|
30
|
+
worktree mount. It has no host home, common Git directory, sibling worktree,
|
|
31
|
+
host temp, shared provider state, secret, Docker/SSH socket, or external route. Its
|
|
32
|
+
short-lived broker grant is revoked during cleanup.
|
|
33
|
+
The grant owns one absolute deadline and every accepted socket. Expiry,
|
|
34
|
+
explicit revoke, credential reload, or broker shutdown aborts an incomplete
|
|
35
|
+
upload or upstream request, destroys accepted sockets, and closes the listener;
|
|
36
|
+
authorization is checked again after the bounded request body is acquired.
|
|
37
|
+
|
|
38
|
+
Codex state lives in a unique, random, immutable-label Docker volume for each
|
|
39
|
+
fresh lineage, never a host or common Codex home. An HMAC-authenticated,
|
|
40
|
+
root-private supervisor registry maps returned session IDs to that exact
|
|
41
|
+
volume. Resume revalidates the volume's owner, namespace, task, and lineage
|
|
42
|
+
labels before every mount. A collision, unlabeled or differently labeled
|
|
43
|
+
preexisting volume, sibling,
|
|
44
|
+
missing, expired, or lost state fails before worker creation.
|
|
45
|
+
Only one worker may mount a lineage at a time; the supervisor holds an atomic
|
|
46
|
+
reservation from preflight consumption through worker cleanup. Expired aliases
|
|
47
|
+
are removed by authenticated periodic GC. The registry retains lineage
|
|
48
|
+
ownership until Docker confirms deletion, retries cleanup failures, and
|
|
49
|
+
reconciles exactly labelled orphan volumes on startup while preserving volumes
|
|
50
|
+
referenced by in-process runs. On restart, exactly owned created, running, or
|
|
51
|
+
exited orphan workers and their networks are removed before volume GC retries.
|
|
52
|
+
An fsync'd, HMAC-authenticated run record binds the generated names, pinned
|
|
53
|
+
image digest, entrypoint policy, private network, state volume, and exact
|
|
54
|
+
worktree mount source/options. Reconciliation inspects that full identity and
|
|
55
|
+
network endpoint membership; forgeable labels or any mismatched name, image,
|
|
56
|
+
mount, option, command, or endpoint are never deletion authority.
|
|
57
|
+
|
|
58
|
+
Preflight verifies Docker, the immutable image, broker readiness, the
|
|
59
|
+
allowlisted non-symlink worktree with no nested mounts, stable device/inode ancestry, and mount policy before prompt, Telegram
|
|
60
|
+
token, dotenv, evidence, resume state, executable override, or provider
|
|
61
|
+
credential reads. The ancestry is revalidated immediately before container
|
|
62
|
+
creation and start, and the immutable entrypoint verifies the mounted worktree
|
|
63
|
+
and reviewed nested cwd before Codex. Only `--model` and `--color` are accepted;
|
|
64
|
+
they are transported as a JSON array and passed exactly once without shell
|
|
65
|
+
splitting. All other provider arguments are rejected before launch.
|
|
66
|
+
Authenticated preflight admission has strict endpoint/field size limits,
|
|
67
|
+
global and per-task caps, and an eager timer sweep, so abandoned capabilities
|
|
68
|
+
expire without requiring a matching `/run`.
|
|
69
|
+
Consumed capabilities enter a separate global/per-task active-run admission
|
|
70
|
+
before validation can yield. The lineage reservation is acquired synchronously
|
|
71
|
+
at that boundary, preventing periodic GC from deleting resume state during
|
|
72
|
+
revalidation. Docker and broker operations have bounded deadlines, and each
|
|
73
|
+
worker has a bounded operator-configured lifetime; timeout cleanup revokes the
|
|
74
|
+
grant and removes the exactly labelled worker and network before capacity is
|
|
75
|
+
released.
|
|
76
|
+
The relay creates one absolute launch deadline before preflight. The same
|
|
77
|
+
deadline and cancellation signal span prompt/stdin and file-secret reads,
|
|
78
|
+
evidence and Telegram setup, Docker setup, broker readiness, execution,
|
|
79
|
+
response/log reads, upstream work and exact cleanup. A disconnected caller or
|
|
80
|
+
supervisor shutdown aborts its run. Independent per-step timeouts cannot extend
|
|
81
|
+
the overall budget, and response bodies are stream-limited to 2 MiB.
|
|
82
|
+
|
|
83
|
+
Codex bypasses its nested sandbox only inside this mandatory outer container.
|
|
84
|
+
Its task-private home contains only that lineage's session state; persisted
|
|
85
|
+
config, rules, hooks, plugins, MCP files and instruction files are rejected
|
|
86
|
+
before Codex starts. MCP is forced empty and project instruction loading is disabled.
|
|
87
|
+
Failure to create the outer boundary is fatal.
|
|
88
|
+
|
|
89
|
+
## Deployment and rotation
|
|
90
|
+
|
|
91
|
+
The default `compose.yaml` contains no provider secret, broker, supervisor, or
|
|
92
|
+
Docker socket. Enable writes only with both files and the explicit profile:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
docker compose -f compose.yaml -f compose.relay-write.yaml --profile relay-write up
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The override fails interpolation unless every required image, control token,
|
|
99
|
+
worktree root, allowlist and credential-file setting is present. Set immutable supervisor, broker, and worker images; a host
|
|
100
|
+
`THREADWIRE_WORKSPACE_ROOT`; JSON `THREADWIRE_ALLOWED_WORKTREE_ROOTS`; separate
|
|
101
|
+
runtime and broker control tokens; `THREADWIRE_ALLOWED_CODEX_MODELS` as a JSON
|
|
102
|
+
server-side model allowlist; a unique `THREADWIRE_STATE_NAMESPACE`; a random
|
|
103
|
+
`THREADWIRE_STATE_AUTH_KEY` of at least 32 characters; and a deployment secret file for the Codex
|
|
104
|
+
credential. The supervisor alone receives `/var/run/docker.sock`. Restrict
|
|
105
|
+
broker egress at the host/firewall to provider endpoints.
|
|
106
|
+
|
|
107
|
+
The supervisor defaults to 16 active runs globally, two per reviewed task, a
|
|
108
|
+
30-second maximum preflight slice, and a one-hour end-to-end launch deadline. Operators may
|
|
109
|
+
lower these finite limits with `THREADWIRE_PREFLIGHT_TIMEOUT_MS`,
|
|
110
|
+
`THREADWIRE_ACTIVE_RUN_CAPACITY`, `THREADWIRE_ACTIVE_TASK_CAPACITY`, and
|
|
111
|
+
`THREADWIRE_WORKER_TIMEOUT_MS`; invalid, zero, or excessive values fail startup.
|
|
112
|
+
|
|
113
|
+
The authenticated registry persists through file fsync, atomic rename, and
|
|
114
|
+
parent-directory fsync. Startup removes only strictly named abandoned temp
|
|
115
|
+
files whose complete contents authenticate under the deployment key; malformed
|
|
116
|
+
or foreign files are left untouched for operator inspection.
|
|
117
|
+
|
|
118
|
+
`THREADWIRE_WORKTREE_VOLUME` is only for nested-Docker/test deployments. Docker
|
|
119
|
+
volume `Subpath` still mounts only the selected task directory. Production uses
|
|
120
|
+
the exact host bind through Docker's structured Mount API with private
|
|
121
|
+
propagation. The supervisor verifies the resulting two-mount inventory before
|
|
122
|
+
start, and the entrypoint independently rejects nested worktree mounts.
|
|
123
|
+
|
|
124
|
+
Replace the broker secret atomically and restart it, or call authenticated
|
|
125
|
+
`/admin/reload`. Reload increments the generation and revokes existing grants.
|
|
126
|
+
Missing, writable, symlinked, empty, or oversized secret files keep it unready.
|
|
127
|
+
|
|
128
|
+
If Docker, broker, pinned image, worktree, security option, or private network
|
|
129
|
+
is unavailable, relay write fails closed. There is no native fallback.
|
|
130
|
+
|
|
131
|
+
## Verification
|
|
132
|
+
|
|
133
|
+
Run `npm run all-checks`, `npm run verify:package`, and
|
|
134
|
+
`npm run test:isolated-runtime-e2e` with broker, runtime, hostile-probe,
|
|
135
|
+
production relay-worker, and fake-upstream image variables documented
|
|
136
|
+
by that script. E2E uses host-volume canaries and fake-upstream authorization
|
|
137
|
+
logs; worker self-report is not the sole assertion.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "threadwire",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Stream Codex, Claude, and OpenCode worker progress to an explicit Telegram destination",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agent",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"private": false,
|
|
15
15
|
"type": "module",
|
|
16
16
|
"files": [
|
|
17
|
+
"CHANGELOG.md",
|
|
17
18
|
"bin/",
|
|
18
19
|
"docs/",
|
|
19
20
|
"scripts/",
|
|
@@ -22,6 +23,8 @@
|
|
|
22
23
|
"threadwire.workspace-profiles.json"
|
|
23
24
|
],
|
|
24
25
|
"bin": {
|
|
26
|
+
"threadwire-isolated-runtime": "bin/isolated-runtime.js",
|
|
27
|
+
"threadwire-model-broker": "bin/model-broker.js",
|
|
25
28
|
"threadwire": "bin/threadwire.js",
|
|
26
29
|
"threadwire-telegram-webhook": "bin/telegram-webhook.js"
|
|
27
30
|
},
|
|
@@ -49,6 +52,7 @@
|
|
|
49
52
|
"prepublishOnly": "npm run all-checks && npm run verify:package",
|
|
50
53
|
"test": "node --test \"test/**/*.test.js\"",
|
|
51
54
|
"test:container-e2e": "sh docker/test/production-e2e.sh",
|
|
55
|
+
"test:isolated-runtime-e2e": "sh docker/test/isolated-runtime-e2e.sh",
|
|
52
56
|
"typecheck": "tsc --noEmit",
|
|
53
57
|
"verify:package": "node scripts/verify-package.js"
|
|
54
58
|
},
|
|
@@ -55,6 +55,24 @@ fi
|
|
|
55
55
|
@@WORKER_SUBCOMMAND_SHIFT@@
|
|
56
56
|
|
|
57
57
|
threadwire_relay_arguments=(run --provider "$THREADWIRE_PROVIDER" --target "$THREADWIRE_TARGET")
|
|
58
|
+
case "${THREADWIRE_RELAY_WRITE:-0}" in
|
|
59
|
+
0) ;;
|
|
60
|
+
1)
|
|
61
|
+
if [[ ! "${THREADWIRE_WORKSPACE_PROFILE:-}" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ ]]; then
|
|
62
|
+
printf '%s\n' 'threadwire: relay write capability requires a valid THREADWIRE_WORKSPACE_PROFILE' >&2
|
|
63
|
+
exit 2
|
|
64
|
+
fi
|
|
65
|
+
if [[ "$THREADWIRE_PROVIDER" != codex ]]; then
|
|
66
|
+
printf '%s\n' 'threadwire: relay write capability is only supported for codex' >&2
|
|
67
|
+
exit 2
|
|
68
|
+
fi
|
|
69
|
+
threadwire_relay_arguments+=(--workspace-profile "$THREADWIRE_WORKSPACE_PROFILE" --relay-write)
|
|
70
|
+
;;
|
|
71
|
+
*)
|
|
72
|
+
printf '%s\n' 'threadwire: relay write capability must be enabled with THREADWIRE_RELAY_WRITE=1' >&2
|
|
73
|
+
exit 2
|
|
74
|
+
;;
|
|
75
|
+
esac
|
|
58
76
|
if [[ -n "${THREADWIRE_PROCESS_NUMBER:-}" ]]; then
|
|
59
77
|
threadwire_relay_arguments+=(--process-number "$THREADWIRE_PROCESS_NUMBER")
|
|
60
78
|
fi
|
|
@@ -14,15 +14,19 @@ const npmEnvironment = {
|
|
|
14
14
|
npm_config_dry_run: "false"
|
|
15
15
|
}
|
|
16
16
|
const EXPECTED_FILES = [
|
|
17
|
+
"CHANGELOG.md",
|
|
17
18
|
"LICENSE",
|
|
18
19
|
"README.md",
|
|
19
20
|
"TELEGRAM-INGRESS.md",
|
|
20
21
|
"bin/telegram-webhook.js",
|
|
21
22
|
"bin/threadwire.js",
|
|
23
|
+
"bin/isolated-runtime.js",
|
|
24
|
+
"bin/model-broker.js",
|
|
22
25
|
"docs/card-10520-plan.md",
|
|
23
26
|
"docs/container-runtime.md",
|
|
24
27
|
"docs/delegated-result-protocol.md",
|
|
25
28
|
"docs/evidence-artifacts.md",
|
|
29
|
+
"docs/isolated-provider-runtime.md",
|
|
26
30
|
"package.json",
|
|
27
31
|
"scripts/atomic-install.js",
|
|
28
32
|
"scripts/install-local-launcher.js",
|
|
@@ -32,11 +36,21 @@ const EXPECTED_FILES = [
|
|
|
32
36
|
"scripts/provider-shims/front-door.sh.template",
|
|
33
37
|
"scripts/provider-shims/opencode-local-fleet.adapter.sh",
|
|
34
38
|
"scripts/verify-package.js",
|
|
39
|
+
"src/absolute-deadline.js",
|
|
35
40
|
"src/activity-log.js",
|
|
41
|
+
"src/normalized-output.js",
|
|
36
42
|
"src/cli.js",
|
|
37
43
|
"src/context-budget-metrics.js",
|
|
38
44
|
"src/delegated-result-admission.js",
|
|
45
|
+
"src/docker-api.js",
|
|
39
46
|
"src/evidence-store.js",
|
|
47
|
+
"src/isolated-runtime-client.js",
|
|
48
|
+
"src/isolated-runtime.js",
|
|
49
|
+
"src/isolated-state.js",
|
|
50
|
+
"src/isolated-worker.js",
|
|
51
|
+
"src/model-broker-policy.js",
|
|
52
|
+
"src/model-broker.js",
|
|
53
|
+
"src/mount-policy.js",
|
|
40
54
|
"src/notice-queue.js",
|
|
41
55
|
"src/notifiers/fetch-transport.js",
|
|
42
56
|
"src/notifiers/telegram.js",
|
|
@@ -46,6 +60,7 @@ const EXPECTED_FILES = [
|
|
|
46
60
|
"src/providers/index.js",
|
|
47
61
|
"src/providers/opencode.js",
|
|
48
62
|
"src/relay.js",
|
|
63
|
+
"src/relay-write.js",
|
|
49
64
|
"src/run-worker.js",
|
|
50
65
|
"src/sentence-buffer.js",
|
|
51
66
|
"src/telegram-ingress/command.js",
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/* eslint-disable jsdoc/require-jsdoc */
|
|
3
|
+
|
|
4
|
+
export class AbsoluteDeadline {
|
|
5
|
+
constructor(expiresAt, {signal, request, response, now = Date.now, timeoutMessage = "Operation timeout", disconnectMessage = "Caller disconnected"} = {}) {
|
|
6
|
+
if (!Number.isSafeInteger(expiresAt) || expiresAt <= now()) throw new Error(timeoutMessage)
|
|
7
|
+
this.expiresAt = expiresAt
|
|
8
|
+
this.now = now
|
|
9
|
+
this.controller = new AbortController()
|
|
10
|
+
this.signal = this.controller.signal
|
|
11
|
+
this.parentSignal = signal
|
|
12
|
+
this.request = request
|
|
13
|
+
this.response = response
|
|
14
|
+
this.socket = request?.socket
|
|
15
|
+
this.timeoutMessage = timeoutMessage
|
|
16
|
+
this.abortParent = () => this.controller.abort(signal.reason instanceof Error ? signal.reason : new Error("Operation aborted"))
|
|
17
|
+
this.abortRequest = () => this.controller.abort(new Error(disconnectMessage))
|
|
18
|
+
this.abortResponse = () => {
|
|
19
|
+
if (!response?.writableEnded) this.abortRequest()
|
|
20
|
+
}
|
|
21
|
+
this.abortSocket = () => {
|
|
22
|
+
if (!response?.writableEnded) this.abortRequest()
|
|
23
|
+
}
|
|
24
|
+
if (signal?.aborted) this.abortParent()
|
|
25
|
+
else signal?.addEventListener("abort", this.abortParent, {once: true})
|
|
26
|
+
request?.once?.("aborted", this.abortRequest)
|
|
27
|
+
response?.once?.("close", this.abortResponse)
|
|
28
|
+
this.socket?.once?.("close", this.abortSocket)
|
|
29
|
+
this.timer = setTimeout(() => this.controller.abort(new Error(timeoutMessage)), Math.max(1, expiresAt - now()))
|
|
30
|
+
this.timer.unref()
|
|
31
|
+
this.signal.addEventListener("abort", () => request?.destroy?.(this.signal.reason), {once: true})
|
|
32
|
+
if (request?.aborted || (response?.destroyed && !response.writableEnded)) this.abortRequest()
|
|
33
|
+
}
|
|
34
|
+
remaining() {
|
|
35
|
+
this.throwIfAborted()
|
|
36
|
+
const remaining = this.expiresAt - this.now()
|
|
37
|
+
if (remaining <= 0) {
|
|
38
|
+
this.controller.abort(new Error(this.timeoutMessage))
|
|
39
|
+
this.throwIfAborted()
|
|
40
|
+
}
|
|
41
|
+
return Math.max(1, remaining)
|
|
42
|
+
}
|
|
43
|
+
options() {
|
|
44
|
+
return {signal: this.signal, timeoutMs: this.remaining()}
|
|
45
|
+
}
|
|
46
|
+
throwIfAborted() {
|
|
47
|
+
if (this.signal.aborted) throw this.signal.reason instanceof Error ? this.signal.reason : new Error("Operation aborted")
|
|
48
|
+
}
|
|
49
|
+
close() {
|
|
50
|
+
clearTimeout(this.timer)
|
|
51
|
+
this.abortParent && this.parentSignal?.removeEventListener?.("abort", this.abortParent)
|
|
52
|
+
this.request?.off?.("aborted", this.abortRequest)
|
|
53
|
+
this.response?.off?.("close", this.abortResponse)
|
|
54
|
+
this.socket?.off?.("close", this.abortSocket)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function deadlineAfter(timeoutMs, options = {}) {
|
|
59
|
+
const now = options.now ?? Date.now
|
|
60
|
+
return new AbsoluteDeadline(now() + timeoutMs, {...options, now})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function abortable(promise, signal) {
|
|
64
|
+
if (signal?.aborted) throw signal.reason
|
|
65
|
+
if (!signal) return promise
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const abort = () => reject(signal.reason instanceof Error ? signal.reason : new Error("Operation aborted"))
|
|
68
|
+
signal.addEventListener("abort", abort, {once: true})
|
|
69
|
+
Promise.resolve(promise).then(resolve, reject).finally(() => signal.removeEventListener("abort", abort))
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function readResponseCapped(response, capacity, signal) {
|
|
74
|
+
const reader = response.body?.getReader()
|
|
75
|
+
if (!reader) return Buffer.alloc(0)
|
|
76
|
+
const chunks = []
|
|
77
|
+
let size = 0
|
|
78
|
+
try {
|
|
79
|
+
while (true) {
|
|
80
|
+
const {done, value} = await abortable(reader.read(), signal)
|
|
81
|
+
if (done) break
|
|
82
|
+
const chunk = Buffer.from(value)
|
|
83
|
+
size += chunk.length
|
|
84
|
+
if (size > capacity) {
|
|
85
|
+
await reader.cancel(new Error("Response exceeded capacity")).catch(() => {})
|
|
86
|
+
throw new Error("Response exceeded capacity")
|
|
87
|
+
}
|
|
88
|
+
chunks.push(chunk)
|
|
89
|
+
}
|
|
90
|
+
return Buffer.concat(chunks, size)
|
|
91
|
+
} finally {
|
|
92
|
+
reader.releaseLock()
|
|
93
|
+
}
|
|
94
|
+
}
|