@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.41da3780c451
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/README.md +88 -2
- package/dist/bridge/bridge-chat.types.d.ts +111 -0
- package/dist/bridge/bridge-chat.types.js +401 -0
- package/dist/bridge/bridge.base.d.ts +4 -1
- package/dist/bridge/bridge.service.d.ts +72 -3
- package/dist/bridge/bridge.service.js +1563 -152
- package/dist/bridge/bridge.types.d.ts +46 -1
- package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
- package/dist/bridge/dispatch-result-outbox.js +62 -0
- package/dist/bridge/dispatch-result-worker.d.ts +2 -0
- package/dist/bridge/dispatch-result-worker.js +122 -0
- package/dist/bridge/internals.d.ts +2 -0
- package/dist/bridge/internals.js +1 -0
- package/dist/bridge/readiness-result-outbox.d.ts +21 -0
- package/dist/bridge/readiness-result-outbox.js +54 -0
- package/dist/bridge/readiness-result-worker.d.ts +2 -0
- package/dist/bridge/readiness-result-worker.js +121 -0
- package/dist/bridge/runner-host-capabilities.d.ts +15 -0
- package/dist/bridge/runner-host-capabilities.js +93 -0
- package/dist/cleanup/cleanup.service.d.ts +1 -0
- package/dist/cleanup/cleanup.service.js +70 -9
- package/dist/cli/cli.help.js +2 -2
- package/dist/cli/cli.service.d.ts +14 -6
- package/dist/cli/cli.service.js +75 -16
- package/dist/cli/local-config.d.ts +1 -0
- package/dist/cli/local-config.js +4 -0
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
- package/dist/runtime/attempt-database-broker.d.ts +89 -0
- package/dist/runtime/attempt-database-broker.js +390 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
- package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
- package/dist/runtime/attempt-path-operation.d.ts +21 -0
- package/dist/runtime/attempt-path-operation.js +94 -0
- package/dist/runtime/attempt-private-directory.d.ts +6 -0
- package/dist/runtime/attempt-private-directory.js +54 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
- package/dist/runtime/attempt-process-supervisor.js +70 -22
- package/dist/runtime/canonical-source-file.d.ts +3 -0
- package/dist/runtime/canonical-source-file.js +54 -0
- package/dist/runtime/docker-engine-probe.d.ts +90 -0
- package/dist/runtime/docker-engine-probe.js +333 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +62 -8
- package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
- package/dist/runtime/host-database-capability-boundary.js +62 -0
- package/dist/runtime/host-diagnostics.d.ts +11 -0
- package/dist/runtime/host-diagnostics.js +192 -0
- package/dist/runtime/host-execution-root.d.ts +4 -0
- package/dist/runtime/host-execution-root.js +39 -0
- package/dist/runtime/host-listener-inspector.d.ts +8 -1
- package/dist/runtime/host-listener-inspector.js +121 -60
- package/dist/runtime/host-runtime-contract.d.ts +12 -1
- package/dist/runtime/host-runtime-contract.js +168 -55
- package/dist/runtime/host-runtime-lease.d.ts +92 -8
- package/dist/runtime/host-runtime-lease.js +1141 -114
- package/dist/runtime/host-runtime-promotion.d.ts +12 -3
- package/dist/runtime/host-runtime-promotion.js +59 -12
- package/dist/runtime/memory-capacity.d.ts +70 -0
- package/dist/runtime/memory-capacity.js +123 -0
- package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
- package/dist/runtime/native-test-postgres-fallback.js +91 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
- package/dist/runtime/node-host-runtime-adapter.js +29 -11
- package/dist/runtime/pinned-directory-process.d.ts +9 -0
- package/dist/runtime/pinned-directory-process.js +61 -0
- package/dist/runtime/pinned-source-directory.d.ts +17 -0
- package/dist/runtime/pinned-source-directory.js +94 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
- package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
- package/dist/runtime/prepared-runtime-identity.js +218 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
- package/dist/runtime/prepared-runtime-readiness.js +285 -0
- package/dist/runtime/runtime-isolation-lease.js +4 -1
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/preflight.d.ts +31 -1
- package/dist/sandbox/preflight.js +237 -51
- package/dist/sandbox/repository-lock.d.ts +2 -1
- package/dist/sandbox/repository-lock.js +19 -9
- package/dist/sandbox/runtime.d.ts +6 -1
- package/dist/sandbox/runtime.js +137 -107
- package/dist/sandbox/sandbox.types.d.ts +7 -0
- package/dist/sandbox/workspace.service.js +2 -0
- package/dist/session/managed-attempt-tools.d.ts +17 -1
- package/dist/session/managed-attempt-tools.js +489 -34
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +106 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +337 -0
- package/dist/session/managed-verification-timeouts.d.ts +8 -0
- package/dist/session/managed-verification-timeouts.js +17 -0
- package/dist/session/phase-completion.artifacts.d.ts +17 -0
- package/dist/session/phase-completion.artifacts.js +224 -22
- package/dist/session/phase-completion.d.ts +11 -1
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +30 -7
- package/dist/session/phase-completion.js +14 -3
- package/dist/session/phase-completion.prompts.js +17 -18
- package/dist/session/phase-completion.runtime.d.ts +3 -1
- package/dist/session/phase-completion.runtime.js +126 -17
- package/dist/session/phase-completion.schemas.js +49 -2
- package/dist/session/phase-completion.validation.d.ts +1 -0
- package/dist/session/phase-completion.validation.js +111 -21
- package/dist/session/pre-publication-validation.d.ts +75 -0
- package/dist/session/pre-publication-validation.js +661 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
- package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
- package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
- package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
- package/dist/session/provider-cli/provider-cli.process.js +422 -22
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +14 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
- package/dist/session/provider-cli/provider-cli.service.js +773 -69
- package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
- package/dist/session/provider-cli/provider-output-state.js +361 -17
- package/dist/session/provider-cli/provider-usage.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.js +28 -5
- package/dist/session/session-redaction.js +434 -9
- package/dist/session/session.service.js +145 -23
- package/dist/session/session.types.d.ts +147 -2
- package/dist/session/session.types.js +587 -7
- package/dist/session/unreferenced-exports.d.ts +12 -0
- package/dist/session/unreferenced-exports.js +59 -0
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/git/git.js +2 -1
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +18 -0
- package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
- package/dist/shared/process/dotnet-toolchain.js +307 -0
- package/dist/shared/process/host-process-recovery.js +3 -2
- package/dist/shared/process/host-process-supervisor.d.ts +2 -0
- package/dist/shared/process/host-process-supervisor.js +60 -11
- package/dist/shared/process/process.d.ts +2 -0
- package/dist/shared/process/process.js +12 -7
- package/dist/shared/provider-capabilities.d.ts +3 -0
- package/dist/shared/provider-capabilities.js +85 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +39 -3
- package/dist/shared/time/monotonic.d.ts +1 -0
- package/dist/shared/time/monotonic.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/dependency-warmer.d.ts +41 -9
- package/dist/worktree/dependency-warmer.js +497 -22
- package/dist/worktree/index.d.ts +1 -1
- package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
- package/dist/worktree/story-qa-reference-hydrator.js +253 -0
- package/dist/worktree/worktree.service.d.ts +7 -0
- package/dist/worktree/worktree.service.js +253 -21
- package/dist/worktree/worktree.types.d.ts +7 -1
- package/package.json +7 -5
- package/runtime/windows-job-supervisor.ps1 +380 -14
package/README.md
CHANGED
|
@@ -31,7 +31,34 @@ The bridge can also read connection settings from environment variables:
|
|
|
31
31
|
- `FORGE_DEVICE_NAME`
|
|
32
32
|
- `FORGE_REPO_ROOT`
|
|
33
33
|
- `FORGE_CLI_PROVIDERS`
|
|
34
|
-
- `FORGE_CODEX_MODELS` / `FORGE_CURSOR_MODELS` (optional comma-separated model ids advertised in runner capabilities)
|
|
34
|
+
- `FORGE_CODEX_MODELS` / `FORGE_CURSOR_MODELS` / `FORGE_CLAUDE_MODELS` (optional comma-separated model ids advertised in runner capabilities)
|
|
35
|
+
- `FORGE_DOTNET_PATH` (optional absolute `dotnet` executable; the bridge otherwise selects one SDK compatible with its mapped repositories)
|
|
36
|
+
|
|
37
|
+
At `connect`, the bridge inspects mapped .NET repositories and the configured repository root,
|
|
38
|
+
selects one installation satisfying their SDK policies, and pins that executable through `PATH` and `DOTNET_ROOT` for dependency warming,
|
|
39
|
+
managed commands, tests, and host-runtime launches. It fails before accepting work when
|
|
40
|
+
no compatible SDK is available instead of falling through to an older ambient SDK.
|
|
41
|
+
During a framework transition, that installation may need both SDK 8 and SDK 10 side by side;
|
|
42
|
+
separate installations do not combine their SDK inventories. Each checkout retains its own
|
|
43
|
+
`global.json` policy. Keep the default clone root narrowly scoped rather than pointing it at
|
|
44
|
+
a parent containing unrelated maintenance checkouts and generated projects.
|
|
45
|
+
|
|
46
|
+
## Provider CLIs
|
|
47
|
+
|
|
48
|
+
The bridge auto-detects these provider CLIs on `PATH` and advertises the ones it finds:
|
|
49
|
+
|
|
50
|
+
| Provider | CLI | Sign-in | Headless invocation |
|
|
51
|
+
| --- | --- | --- | --- |
|
|
52
|
+
| `codex` | `codex` | `codex login` | `codex exec --json` |
|
|
53
|
+
| `cursor` | `cursor-agent` | `cursor-agent login` | `cursor-agent --print --output-format stream-json` |
|
|
54
|
+
| `claude` | `claude` (Claude Code) | `claude` then `/login`, or an Anthropic API key granted by Forge | `claude -p --output-format stream-json --verbose` |
|
|
55
|
+
|
|
56
|
+
Claude Code notes:
|
|
57
|
+
|
|
58
|
+
- Install with the native installer or `npm install -g @anthropic-ai/claude-code`. On Windows, Claude Code needs Git for Windows; set `CLAUDE_CODE_GIT_BASH_PATH` if `bash.exe` is not on `PATH`. `CLAUDE_CONFIG_DIR` is passed through if you keep config outside `~/.claude`.
|
|
59
|
+
- Forge pipes the prompt on stdin, runs with `--permission-mode bypassPermissions` inside the managed worktree, and registers its completion MCP server through a temporary `--mcp-config` file outside the worktree with `--strict-mcp-config`, so user-level MCP servers are not loaded into unattended runs. Forge tools appear to the model as `mcp__forge__<tool>`.
|
|
60
|
+
- Claude Code has no model-listing command, so the bridge advertises the `sonnet`, `opus`, and `haiku` aliases. Set `FORGE_CLAUDE_MODELS` to advertise specific model ids instead.
|
|
61
|
+
- Claude runs in host mode only. Docker sandbox placement currently supports Codex only.
|
|
35
62
|
|
|
36
63
|
Show or change the saved server without editing `bridge.json`:
|
|
37
64
|
|
|
@@ -63,6 +90,45 @@ forge-bridge add-repo Forge /path/to/Forge
|
|
|
63
90
|
forge-bridge remove-repo Forge
|
|
64
91
|
```
|
|
65
92
|
|
|
93
|
+
Readiness probes and Story dispatch select the same local mapping: repository ID first,
|
|
94
|
+
then a remote-only mapping, then a legacy name-only mapping. Explicit mappings may point
|
|
95
|
+
outside the default clone root; both paths use the mapped checkout's parent without moving
|
|
96
|
+
existing managed worktrees. A readiness request must name the same fully qualified local
|
|
97
|
+
checkout. Missing, ambiguous or mismatched mappings fail before runtime preparation; a
|
|
98
|
+
server-supplied path never grants access to a different repository.
|
|
99
|
+
|
|
100
|
+
Before narrowing a runner's default root, inventory its advertisements, explicitly retain
|
|
101
|
+
every approved or intended executable mapping and its QA launch targets, and document any
|
|
102
|
+
intentional exclusions. An omitted repository mapping can lose its server-side execution
|
|
103
|
+
approval. Reconcile a stale mapping rather than widening filesystem
|
|
104
|
+
access to make a failed readiness request pass.
|
|
105
|
+
|
|
106
|
+
## Host Docker engine capability
|
|
107
|
+
|
|
108
|
+
The bridge probes ordinary Docker-daemon access under its own process identity and advertises the
|
|
109
|
+
generic `docker_engine` execution facility independently from Docker sandbox placement. A Story whose
|
|
110
|
+
QA scope requires `docker_engine` is admitted only to a managed host runner with a fresh successful
|
|
111
|
+
probe. This supports repository-owned Testcontainers, Compose services, and other container tests;
|
|
112
|
+
the repository remains responsible for database and image versions. Forge agent sandboxes do not
|
|
113
|
+
receive the host Docker socket, so a Docker-engine requirement is not eligible for
|
|
114
|
+
`executionMode: docker`. The probe is refreshed on heartbeats and again immediately before a matching
|
|
115
|
+
dispatch.
|
|
116
|
+
|
|
117
|
+
On developer Windows runners, local Docker Desktop recovery can be explicitly enabled by setting
|
|
118
|
+
`dockerDesktopRecoveryEnabled` to `true` in the bridge's local `bridge.json`, or with the
|
|
119
|
+
`FORGE_DOCKER_DESKTOP_AUTO_RECOVERY=true` operator override. The bridge waits for three consecutive
|
|
120
|
+
failed daemon probes plus a fresh confirmation probe after a 30-second grace period. It recovers only
|
|
121
|
+
the local `default` or `desktop-linux` context, and only while it owns no active session or
|
|
122
|
+
reservation. If Docker Desktop is already stopped, recovery starts it without issuing shutdown or
|
|
123
|
+
WSL termination commands. Before relaunch, it removes only verified orphaned Docker companion
|
|
124
|
+
processes whose executable is inside the Docker installation and whose parent is no longer a valid
|
|
125
|
+
Docker backend. A running but unresponsive Desktop is stopped, allowed to settle, and relaunched
|
|
126
|
+
with bounded retries that reject an immediately exiting process. Every recovery is
|
|
127
|
+
single-flight, must pass a fresh `docker info` probe before the capability is advertised again, and
|
|
128
|
+
enters a ten-minute cooldown after an unsuccessful attempt. Remote `DOCKER_HOST` endpoints are never
|
|
129
|
+
restarted. Operators can tune the bounded policy with `FORGE_DOCKER_RECOVERY_FAILURE_THRESHOLD`,
|
|
130
|
+
`FORGE_DOCKER_RECOVERY_COOLDOWN_MS`, and `FORGE_DOCKER_RECOVERY_STARTUP_TIMEOUT_MS`.
|
|
131
|
+
|
|
66
132
|
## Docker sandbox runners
|
|
67
133
|
|
|
68
134
|
Protocol-v2 bridges preflight Docker before advertising the `docker` sandbox runtime. Validate a repository contract and the local runner before enabling a project's `sandbox_required` policy:
|
|
@@ -100,8 +166,11 @@ Production shared runners require a Linux rootless Docker daemon. Docker Desktop
|
|
|
100
166
|
- `FORGE_MAX_CONCURRENT_SANDBOX_BUILDS` / `FORGE_SANDBOX_PID_CAPACITY` — weighted build and PID admission ceilings (defaults: `1` and `2048`).
|
|
101
167
|
- `FORGE_DOCKER_CONTEXT` / `FORGE_DOCKER_EXECUTABLE` — dedicated daemon selection.
|
|
102
168
|
- `FORGE_SANDBOX_STATE_ROOT` — fsync'd attempt ledgers, restricted diagnostics, and build state.
|
|
169
|
+
- `FORGE_SANDBOX_EXECUTION_ROOT` — optional absolute root for disposable managed-host temp/cache/test trees. Windows defaults to the compact per-user `%USERPROFILE%\.fgx` root and rejects a configured root that cannot fit Forge's nested Git/test path budget before an agent starts. Durable ledgers and retained diagnostics remain under `FORGE_SANDBOX_STATE_ROOT`.
|
|
103
170
|
- `FORGE_DISK_HIGH_WATERMARK_PERCENT` / `FORGE_DISK_LOW_WATERMARK_PERCENT` / `FORGE_MINIMUM_WORKSPACE_BYTES` — admission watermarks with high/low hysteresis.
|
|
104
|
-
-
|
|
171
|
+
- Per-attempt workspace reservations are calculated from the repository source footprint, detected Node/.NET/browser toolchains, the execution role, and immutable sandbox app services. The API uses that request for local and Bridge placement; a Bridge rechecks its authoritative checkout and Docker contract before acceptance. Shared Docker images and package caches remain runner-level capacity, while `FORGE_MINIMUM_WORKSPACE_BYTES` remains a separate host safety reserve rather than a per-lane charge.
|
|
172
|
+
- `FORGE_SANDBOX_HOST_MEMORY_RESERVE_BYTES` — explicit host-memory safety-reserve override for sandbox admission. Without an override, Forge derives a bounded reserve from the sampled capacity domain (Linux: 12.5%, 1–4 GiB; Docker Desktop development: 6.25%, 1–2 GiB). The floor, ceiling, and percentage are configurable with `FORGE_SANDBOX_HOST_MEMORY_RESERVE_FLOOR_BYTES`, `FORGE_SANDBOX_HOST_MEMORY_RESERVE_CEILING_BYTES`, and `FORGE_SANDBOX_HOST_MEMORY_RESERVE_PERCENT`.
|
|
173
|
+
- Managed-host admission uses the same bounded 12.5% physical-memory policy through `FORGE_HOST_MEMORY_RESERVE_BYTES` or its `_FLOOR_BYTES`, `_CEILING_BYTES`, and `_PERCENT` controls. While no Forge attempt holds capacity, the first worker is admitted against the configured reserve floor (1 GiB by default); `FORGE_HOST_IDLE_MEMORY_RESERVE_BYTES` can override that idle reserve. Once an attempt is admitted, every additional admission enforces the full target reserve. Pressure is applied immediately; recovery requires two samples one second apart by default (`FORGE_HOST_MEMORY_RECOVERY_SAMPLES` and `FORGE_HOST_MEMORY_RECOVERY_SAMPLE_INTERVAL_MS`). `FORGE_HOST_DEFAULT_WORKER_MEMORY_LIMIT_BYTES` configures only the conservative cold-start fallback for legacy packets that omit an exact worker request; current packets remain governed by their explicit role request.
|
|
105
174
|
- `FORGE_SANDBOX_MEMORY_SAMPLE_INTERVAL_MS` — interval for sampled Docker peak-memory telemetry (default `1000` ms, clamped to `250`–`30000` ms). Sampling is diagnostic and never changes an attempt outcome.
|
|
106
175
|
- `FORGE_SANDBOX_DEDICATED_DISK=true` — operator assertion that sandbox storage is on a dedicated, conservatively watermarked disk when the Docker storage driver cannot enforce per-volume quotas. `sandbox_required` fails placement unless either quota support or this assertion is advertised.
|
|
107
176
|
- `FORGE_SANDBOX_IMAGE_RETENTION_COUNT` / `FORGE_SANDBOX_IMAGE_RETENTION_TTL_MS` — per-project image-cache retention (defaults: 3 images and 7 days).
|
|
@@ -127,10 +196,27 @@ The bridge reaps exact-labeled orphan resources before connecting and every minu
|
|
|
127
196
|
|
|
128
197
|
Every attempt-owned container (including short-lived initialization helpers), network, and volume carries `dev.forge.resource-lease-id` equal to the dispatch execution lease. Ledger schema v2 persists the same `resourceLeaseId`; teardown compares the complete expected label set, and orphan reaping fails closed unless the managed, resource-lease, execution-lease, attempt, runner, runner-instance, and fencing identities are all present and consistent.
|
|
129
198
|
|
|
199
|
+
Host-mode diagnostics retain logs and test results under the 200 MiB archive budget, but omit reproducible `.dll` and `.pdb` copies from `test-output`; the archive manifest records their count and bytes. If other evidence exceeds the budget after supervised children and adapters have stopped, the Bridge releases runtime capacity while preserving the original attempt directory and a released ledger for restricted operator review. It does not delete over-budget original evidence automatically. Process-cleanup failures and unsafe evidence paths still fail closed and retain the runtime claim.
|
|
200
|
+
|
|
130
201
|
In `bridge_push` mode, the start packet carries only an attempt/fence/branch-bound publication handle. After validating and importing the output bundle, the authenticated current bridge redeems that handle for an in-memory, exact-repository credential; the API rechecks the live lease/fence/grant/branch/result SHA and records redemption before returning it. Git receives the credential through a process environment configuration, never command arguments, files, Git config, or the container. The bridge compares the remote branch with the immutable input commit and pushes only the exact result commit using `--force-with-lease`; cancellation aborts Git, an already-published matching result is an idempotent success, and any other remote value is rejected as stale. The API records the fenced publication acknowledgment before sign-off and does not push the branch a second time while opening the pull request.
|
|
131
202
|
|
|
132
203
|
Failed project, tooling, and agent image stages preserve only a redacted, capped, mode-0600 diagnostic at `FORGE_SANDBOX_STATE_ROOT/build-logs/<image-key>/<stage>.log`. The event stream carries its digest, byte count, and truncation flag, never the log contents. Treat this directory as restricted source-bearing audit data and apply local retention controls.
|
|
133
204
|
|
|
205
|
+
## Owned test evidence
|
|
206
|
+
|
|
207
|
+
`npm test` closes a manifest over the complete Bridge source, test, runtime, configuration, workflow,
|
|
208
|
+
and imported fixture set before compilation. It records the Git commit and exact Node, npm, Git, .NET,
|
|
209
|
+
TypeScript, Vitest, OS, and temporary-root identities, compiles into an owned temporary root, runs
|
|
210
|
+
Vitest through the Windows process-tree supervisor, revalidates the source and toolchain, independently
|
|
211
|
+
rehashes the compiled output, and removes the exact owner root before reporting success.
|
|
212
|
+
|
|
213
|
+
Set `FORGE_BRIDGE_SUITE_RECEIPT_PATH` to an absolute path for a nonexistent output file to retain the
|
|
214
|
+
verified execution receipt. On Windows evidence runs, `TEMP`, `TMP`, `TMPDIR`, and `FORGE_TMPDIR` may
|
|
215
|
+
point to the same short, empty, high-capacity root. This changes only owned temporary-file placement; it
|
|
216
|
+
does not weaken disk, memory, cleanup, or Docker preflight policy. A Bridge receipt remains
|
|
217
|
+
`fullGateAttested=false` until the suite is registered with the live residue auditor and the required
|
|
218
|
+
source/toolchain-identical repeat evidence is complete.
|
|
219
|
+
|
|
134
220
|
## Package smoke check
|
|
135
221
|
|
|
136
222
|
From this package directory:
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export declare const bridgeChatProtocolVersion = "chat_interactive_v1";
|
|
2
|
+
export declare const bridgeChatUpdateRequiredCode = "update_required";
|
|
3
|
+
export declare const bridgeChatMaxCallbackPayloadBytes: number;
|
|
4
|
+
export declare const bridgeChatMaxProviderThreadIdChars = 256;
|
|
5
|
+
export type BridgeChatAvailability = "available" | "degraded" | "unavailable";
|
|
6
|
+
export type BridgeChatRequestedCapability = "text" | "cancel" | "resume" | "thread_continuity" | "attachment_handoffs" | "action_broker_grants";
|
|
7
|
+
export type ChatTurnCallbackKind = "turn_started" | "output_delta" | "output_completed" | "failed" | "cancelled" | "expired";
|
|
8
|
+
export type ChatTurnCallbackRedactionStatus = "redacted" | "not_applicable";
|
|
9
|
+
export interface BridgeChatCapability {
|
|
10
|
+
readonly availability: BridgeChatAvailability;
|
|
11
|
+
readonly protocolVersion: string;
|
|
12
|
+
readonly supportedProviders: readonly string[];
|
|
13
|
+
readonly supportedModels: readonly string[];
|
|
14
|
+
readonly supportsTextStreaming: boolean;
|
|
15
|
+
readonly supportsCancel: boolean;
|
|
16
|
+
readonly supportsResume: boolean;
|
|
17
|
+
readonly supportsProviderThreadContinuity: boolean;
|
|
18
|
+
readonly supportsAttachments: boolean;
|
|
19
|
+
readonly supportsActionBrokerGrants: boolean;
|
|
20
|
+
readonly maxMessageBytes: number;
|
|
21
|
+
readonly maxContextRefs: number;
|
|
22
|
+
readonly unavailableCode?: string | null;
|
|
23
|
+
readonly unavailableReason?: string | null;
|
|
24
|
+
}
|
|
25
|
+
export interface ChatTurnDispatchPacket {
|
|
26
|
+
readonly chatRunId: string;
|
|
27
|
+
readonly conversationId: string;
|
|
28
|
+
readonly turnId: string;
|
|
29
|
+
readonly leaseId: string;
|
|
30
|
+
readonly fencingToken: string;
|
|
31
|
+
readonly runnerInstanceId: string;
|
|
32
|
+
readonly providerKind: string;
|
|
33
|
+
readonly model: string;
|
|
34
|
+
readonly messages: readonly ChatTurnMessage[];
|
|
35
|
+
readonly contextRefs: readonly ChatTurnContextRef[];
|
|
36
|
+
readonly idempotencyKey: string;
|
|
37
|
+
readonly providerThreadId?: string | null;
|
|
38
|
+
readonly protocolVersion: typeof bridgeChatProtocolVersion;
|
|
39
|
+
readonly requestedCapabilities: readonly BridgeChatRequestedCapability[];
|
|
40
|
+
readonly attachmentHandoffs: readonly ChatAttachmentHandoffHandle[];
|
|
41
|
+
readonly actionBrokerGrants: readonly ChatActionBrokerGrant[];
|
|
42
|
+
}
|
|
43
|
+
export interface ChatTurnMessage {
|
|
44
|
+
readonly role: string;
|
|
45
|
+
readonly content: string;
|
|
46
|
+
readonly metadata?: Readonly<Record<string, unknown>> | null;
|
|
47
|
+
}
|
|
48
|
+
export interface ChatTurnContextRef {
|
|
49
|
+
readonly kind: string;
|
|
50
|
+
readonly refId: string;
|
|
51
|
+
readonly title?: string | null;
|
|
52
|
+
readonly summary?: string | null;
|
|
53
|
+
readonly metadata?: Readonly<Record<string, unknown>> | null;
|
|
54
|
+
}
|
|
55
|
+
export interface ChatAttachmentHandoffHandle {
|
|
56
|
+
readonly turnId: string;
|
|
57
|
+
readonly attachmentId: string;
|
|
58
|
+
readonly tempFileToken: string;
|
|
59
|
+
readonly mimeType: string;
|
|
60
|
+
readonly sizeBytes: number;
|
|
61
|
+
readonly sha256: string;
|
|
62
|
+
readonly fileNameDisplay: string;
|
|
63
|
+
readonly expiresAt: string;
|
|
64
|
+
readonly status: "available";
|
|
65
|
+
}
|
|
66
|
+
export interface ChatActionBrokerGrant {
|
|
67
|
+
readonly grantId: string;
|
|
68
|
+
readonly turnId: string;
|
|
69
|
+
readonly leaseId: string;
|
|
70
|
+
readonly fencingToken: string;
|
|
71
|
+
readonly allowedScopes: readonly string[];
|
|
72
|
+
readonly nonce: string;
|
|
73
|
+
readonly expiresAt: string;
|
|
74
|
+
readonly redactionStatus: ChatTurnCallbackRedactionStatus;
|
|
75
|
+
}
|
|
76
|
+
export interface ChatTurnCallbackEnvelope {
|
|
77
|
+
readonly callbackId: string;
|
|
78
|
+
readonly chatRunId: string;
|
|
79
|
+
readonly conversationId: string;
|
|
80
|
+
readonly turnId: string;
|
|
81
|
+
readonly leaseId: string;
|
|
82
|
+
readonly fencingToken: string;
|
|
83
|
+
readonly runnerId: string;
|
|
84
|
+
readonly runnerInstanceId: string;
|
|
85
|
+
readonly sequence: number;
|
|
86
|
+
readonly eventType: ChatTurnCallbackKind;
|
|
87
|
+
readonly redactionStatus: ChatTurnCallbackRedactionStatus;
|
|
88
|
+
readonly providerThreadId?: string | null;
|
|
89
|
+
readonly payload?: Readonly<Record<string, unknown>> | null;
|
|
90
|
+
readonly occurredAt?: string | null;
|
|
91
|
+
readonly protocolVersion: typeof bridgeChatProtocolVersion;
|
|
92
|
+
}
|
|
93
|
+
export interface ChatTurnCancelPacket {
|
|
94
|
+
readonly chatRunId?: string | null;
|
|
95
|
+
readonly conversationId?: string | null;
|
|
96
|
+
readonly turnId?: string | null;
|
|
97
|
+
readonly leaseId: string;
|
|
98
|
+
readonly fencingToken: string;
|
|
99
|
+
readonly runnerId?: string | null;
|
|
100
|
+
readonly runnerInstanceId?: string | null;
|
|
101
|
+
readonly providerThreadId?: string | null;
|
|
102
|
+
readonly reason?: string | null;
|
|
103
|
+
readonly payload?: Readonly<Record<string, unknown>> | null;
|
|
104
|
+
readonly requestedAt?: string | null;
|
|
105
|
+
readonly protocolVersion?: string | null;
|
|
106
|
+
}
|
|
107
|
+
export declare function unavailableBridgeChatCapability(protocolVersion?: string, unavailableCode?: string | null, unavailableReason?: string | null): BridgeChatCapability;
|
|
108
|
+
export declare function normalizeBridgeChatCapability(value: BridgeChatCapability | null | undefined): BridgeChatCapability;
|
|
109
|
+
export declare function parseChatTurnDispatchPacket(value: unknown, capability?: BridgeChatCapability): ChatTurnDispatchPacket;
|
|
110
|
+
export declare function isTerminalChatTurnCallbackKind(kind: ChatTurnCallbackKind): boolean;
|
|
111
|
+
export declare function assertBridgeChatCallbackPayload(value: unknown, label?: string): void;
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
export const bridgeChatProtocolVersion = "chat_interactive_v1";
|
|
2
|
+
export const bridgeChatUpdateRequiredCode = "update_required";
|
|
3
|
+
export const bridgeChatMaxCallbackPayloadBytes = 256 * 1024;
|
|
4
|
+
export const bridgeChatMaxProviderThreadIdChars = 256;
|
|
5
|
+
const supportedAvailability = new Set(["available", "degraded", "unavailable"]);
|
|
6
|
+
const supportedRequestedCapabilities = new Set([
|
|
7
|
+
"text",
|
|
8
|
+
"cancel",
|
|
9
|
+
"resume",
|
|
10
|
+
"thread_continuity",
|
|
11
|
+
"attachment_handoffs",
|
|
12
|
+
"action_broker_grants",
|
|
13
|
+
]);
|
|
14
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
15
|
+
const unixEpochMs = Date.parse("1970-01-01T00:00:00.000Z");
|
|
16
|
+
const maxOpaqueHandleChars = 4096;
|
|
17
|
+
const maxGrantNonceChars = 4096;
|
|
18
|
+
const maxMetadataStringChars = 4096;
|
|
19
|
+
export function unavailableBridgeChatCapability(protocolVersion = bridgeChatProtocolVersion, unavailableCode, unavailableReason) {
|
|
20
|
+
return {
|
|
21
|
+
availability: "unavailable",
|
|
22
|
+
protocolVersion: nonEmptyStringOr(protocolVersion, bridgeChatProtocolVersion),
|
|
23
|
+
supportedProviders: [],
|
|
24
|
+
supportedModels: [],
|
|
25
|
+
supportsTextStreaming: false,
|
|
26
|
+
supportsCancel: false,
|
|
27
|
+
supportsResume: false,
|
|
28
|
+
supportsProviderThreadContinuity: false,
|
|
29
|
+
supportsAttachments: false,
|
|
30
|
+
supportsActionBrokerGrants: false,
|
|
31
|
+
maxMessageBytes: 0,
|
|
32
|
+
maxContextRefs: 0,
|
|
33
|
+
unavailableCode: cleanOptionalString(unavailableCode),
|
|
34
|
+
unavailableReason: cleanOptionalString(unavailableReason),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function normalizeBridgeChatCapability(value) {
|
|
38
|
+
if (!value) {
|
|
39
|
+
return unavailableBridgeChatCapability();
|
|
40
|
+
}
|
|
41
|
+
const protocolVersion = nonEmptyStringOr(value.protocolVersion, bridgeChatProtocolVersion);
|
|
42
|
+
if (protocolVersion !== bridgeChatProtocolVersion) {
|
|
43
|
+
return unavailableBridgeChatCapability(protocolVersion, bridgeChatUpdateRequiredCode, `Bridge Chat protocol '${protocolVersion}' is not supported by this bridge.`);
|
|
44
|
+
}
|
|
45
|
+
const availability = supportedAvailability.has(value.availability) ? value.availability : "unavailable";
|
|
46
|
+
return {
|
|
47
|
+
availability,
|
|
48
|
+
protocolVersion: bridgeChatProtocolVersion,
|
|
49
|
+
supportedProviders: cleanStringArray(value.supportedProviders, true),
|
|
50
|
+
supportedModels: cleanStringArray(value.supportedModels, false),
|
|
51
|
+
supportsTextStreaming: value.supportsTextStreaming === true,
|
|
52
|
+
supportsCancel: value.supportsCancel === true,
|
|
53
|
+
supportsResume: value.supportsResume === true,
|
|
54
|
+
supportsProviderThreadContinuity: value.supportsProviderThreadContinuity === true,
|
|
55
|
+
supportsAttachments: value.supportsAttachments === true,
|
|
56
|
+
supportsActionBrokerGrants: value.supportsActionBrokerGrants === true,
|
|
57
|
+
maxMessageBytes: nonNegativeInteger(value.maxMessageBytes),
|
|
58
|
+
maxContextRefs: nonNegativeInteger(value.maxContextRefs),
|
|
59
|
+
unavailableCode: availability === "unavailable" ? cleanOptionalString(value.unavailableCode) : null,
|
|
60
|
+
unavailableReason: availability === "unavailable" ? cleanOptionalString(value.unavailableReason) : null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function parseChatTurnDispatchPacket(value, capability = unavailableBridgeChatCapability()) {
|
|
64
|
+
const packet = asObject(value, "chat turn dispatch packet");
|
|
65
|
+
const protocolVersion = requiredString(packet, "protocolVersion");
|
|
66
|
+
if (protocolVersion !== bridgeChatProtocolVersion) {
|
|
67
|
+
throw new Error(`Unsupported chat protocolVersion: ${protocolVersion}`);
|
|
68
|
+
}
|
|
69
|
+
assertReservedFieldsAbsent(packet, "chat turn dispatch packet");
|
|
70
|
+
const normalizedCapability = normalizeBridgeChatCapability(capability);
|
|
71
|
+
const messages = parseMessages(packet.messages, normalizedCapability.maxMessageBytes);
|
|
72
|
+
const contextRefs = parseContextRefs(packet.contextRefs);
|
|
73
|
+
if (normalizedCapability.maxContextRefs > 0 && contextRefs.length > normalizedCapability.maxContextRefs) {
|
|
74
|
+
throw new Error("contextRefs exceeds maxContextRefs.");
|
|
75
|
+
}
|
|
76
|
+
const turnId = requiredUuid(packet, "turnId");
|
|
77
|
+
const leaseId = requiredUuid(packet, "leaseId");
|
|
78
|
+
const fencingToken = requiredUuid(packet, "fencingToken");
|
|
79
|
+
const requestedCapabilities = parseRequestedCapabilities(packet.requestedCapabilities, normalizedCapability);
|
|
80
|
+
const attachmentHandoffs = parseAttachmentHandoffs(packet.attachmentHandoffs, normalizedCapability, requestedCapabilities);
|
|
81
|
+
const actionBrokerGrants = parseActionBrokerGrants(packet.actionBrokerGrants, normalizedCapability, requestedCapabilities);
|
|
82
|
+
assertBoundedEnvelopesMatchFence(attachmentHandoffs, actionBrokerGrants, turnId, leaseId, fencingToken);
|
|
83
|
+
return {
|
|
84
|
+
chatRunId: requiredUuid(packet, "chatRunId"),
|
|
85
|
+
conversationId: requiredUuid(packet, "conversationId"),
|
|
86
|
+
turnId,
|
|
87
|
+
leaseId,
|
|
88
|
+
fencingToken,
|
|
89
|
+
runnerInstanceId: requiredUuid(packet, "runnerInstanceId"),
|
|
90
|
+
providerKind: requiredString(packet, "providerKind").trim().toLowerCase(),
|
|
91
|
+
model: requiredString(packet, "model").trim(),
|
|
92
|
+
messages,
|
|
93
|
+
contextRefs,
|
|
94
|
+
idempotencyKey: requiredString(packet, "idempotencyKey").trim(),
|
|
95
|
+
providerThreadId: optionalString(packet, "providerThreadId", bridgeChatMaxProviderThreadIdChars),
|
|
96
|
+
protocolVersion: bridgeChatProtocolVersion,
|
|
97
|
+
requestedCapabilities,
|
|
98
|
+
attachmentHandoffs,
|
|
99
|
+
actionBrokerGrants,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function isTerminalChatTurnCallbackKind(kind) {
|
|
103
|
+
return kind === "output_completed" || kind === "failed" || kind === "cancelled" || kind === "expired";
|
|
104
|
+
}
|
|
105
|
+
export function assertBridgeChatCallbackPayload(value, label = "payload") {
|
|
106
|
+
if (value === undefined || value === null) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
110
|
+
throw new Error(`${label} must be an object when provided.`);
|
|
111
|
+
}
|
|
112
|
+
if (new TextEncoder().encode(JSON.stringify(value)).byteLength > bridgeChatMaxCallbackPayloadBytes) {
|
|
113
|
+
throw new Error(`${label} exceeds maxCallbackPayloadBytes.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function parseMessages(value, maxMessageBytes) {
|
|
117
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
118
|
+
throw new Error("messages must be a non-empty array.");
|
|
119
|
+
}
|
|
120
|
+
return value.map((entry, index) => {
|
|
121
|
+
const message = asObject(entry, `messages[${index}]`);
|
|
122
|
+
assertReservedFieldsAbsent(message, `messages[${index}]`);
|
|
123
|
+
const content = requiredString(message, "content");
|
|
124
|
+
if (maxMessageBytes > 0 && new TextEncoder().encode(content).byteLength > maxMessageBytes) {
|
|
125
|
+
throw new Error(`messages[${index}].content exceeds maxMessageBytes.`);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
role: requiredString(message, "role").trim(),
|
|
129
|
+
content,
|
|
130
|
+
metadata: optionalObject(message.metadata, `messages[${index}].metadata`),
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function parseContextRefs(value) {
|
|
135
|
+
if (value === undefined || value === null) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(value)) {
|
|
139
|
+
throw new Error("contextRefs must be an array when provided.");
|
|
140
|
+
}
|
|
141
|
+
return value.map((entry, index) => {
|
|
142
|
+
const ref = asObject(entry, `contextRefs[${index}]`);
|
|
143
|
+
assertReservedFieldsAbsent(ref, `contextRefs[${index}]`);
|
|
144
|
+
return {
|
|
145
|
+
kind: requiredString(ref, "kind").trim(),
|
|
146
|
+
refId: requiredString(ref, "refId").trim(),
|
|
147
|
+
title: optionalString(ref, "title"),
|
|
148
|
+
summary: optionalString(ref, "summary"),
|
|
149
|
+
metadata: optionalObject(ref.metadata, `contextRefs[${index}].metadata`),
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function parseAttachmentHandoffs(value, capability, requestedCapabilities) {
|
|
154
|
+
if (value === undefined || value === null) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
if (!Array.isArray(value)) {
|
|
158
|
+
throw new Error("attachmentHandoffs must be an array when provided.");
|
|
159
|
+
}
|
|
160
|
+
if (value.length > 0 && !capability.supportsAttachments) {
|
|
161
|
+
throw new Error("Bridge Chat attachmentHandoffs require supportsAttachments.");
|
|
162
|
+
}
|
|
163
|
+
if (value.length > 0 && !requestedCapabilities.includes("attachment_handoffs")) {
|
|
164
|
+
throw new Error("Bridge Chat attachmentHandoffs require requestedCapabilities to include attachment_handoffs.");
|
|
165
|
+
}
|
|
166
|
+
return value.map((entry, index) => {
|
|
167
|
+
const handoff = asObject(entry, `attachmentHandoffs[${index}]`);
|
|
168
|
+
assertOnlyKeys(handoff, `attachmentHandoffs[${index}]`, [
|
|
169
|
+
"turnId",
|
|
170
|
+
"attachmentId",
|
|
171
|
+
"tempFileToken",
|
|
172
|
+
"mimeType",
|
|
173
|
+
"sizeBytes",
|
|
174
|
+
"sha256",
|
|
175
|
+
"fileNameDisplay",
|
|
176
|
+
"expiresAt",
|
|
177
|
+
"status",
|
|
178
|
+
]);
|
|
179
|
+
const status = requiredString(handoff, "status").trim();
|
|
180
|
+
if (status !== "available") {
|
|
181
|
+
throw new Error(`attachmentHandoffs[${index}].status must be available.`);
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
turnId: requiredUuid(handoff, "turnId"),
|
|
185
|
+
attachmentId: requiredUuid(handoff, "attachmentId"),
|
|
186
|
+
tempFileToken: boundedRequiredString(handoff, "tempFileToken", maxOpaqueHandleChars),
|
|
187
|
+
mimeType: boundedRequiredString(handoff, "mimeType", maxMetadataStringChars),
|
|
188
|
+
sizeBytes: requiredNonNegativeInteger(handoff, "sizeBytes"),
|
|
189
|
+
sha256: boundedRequiredString(handoff, "sha256", maxMetadataStringChars),
|
|
190
|
+
fileNameDisplay: boundedRequiredString(handoff, "fileNameDisplay", maxMetadataStringChars),
|
|
191
|
+
expiresAt: requiredFutureContractTimestamp(handoff, "expiresAt"),
|
|
192
|
+
status,
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function parseActionBrokerGrants(value, capability, requestedCapabilities) {
|
|
197
|
+
if (value === undefined || value === null) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
if (!Array.isArray(value)) {
|
|
201
|
+
throw new Error("actionBrokerGrants must be an array when provided.");
|
|
202
|
+
}
|
|
203
|
+
if (value.length > 0 && !capability.supportsActionBrokerGrants) {
|
|
204
|
+
throw new Error("Bridge Chat actionBrokerGrants require supportsActionBrokerGrants.");
|
|
205
|
+
}
|
|
206
|
+
if (value.length > 0 && !requestedCapabilities.includes("action_broker_grants")) {
|
|
207
|
+
throw new Error("Bridge Chat actionBrokerGrants require requestedCapabilities to include action_broker_grants.");
|
|
208
|
+
}
|
|
209
|
+
return value.map((entry, index) => {
|
|
210
|
+
const grant = asObject(entry, `actionBrokerGrants[${index}]`);
|
|
211
|
+
assertOnlyKeys(grant, `actionBrokerGrants[${index}]`, [
|
|
212
|
+
"grantId",
|
|
213
|
+
"turnId",
|
|
214
|
+
"leaseId",
|
|
215
|
+
"fencingToken",
|
|
216
|
+
"allowedScopes",
|
|
217
|
+
"nonce",
|
|
218
|
+
"expiresAt",
|
|
219
|
+
"redactionStatus",
|
|
220
|
+
]);
|
|
221
|
+
const redactionStatus = requiredString(grant, "redactionStatus").trim();
|
|
222
|
+
if (redactionStatus !== "redacted" && redactionStatus !== "not_applicable") {
|
|
223
|
+
throw new Error(`actionBrokerGrants[${index}].redactionStatus is unsupported.`);
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
grantId: requiredUuid(grant, "grantId"),
|
|
227
|
+
turnId: requiredUuid(grant, "turnId"),
|
|
228
|
+
leaseId: requiredUuid(grant, "leaseId"),
|
|
229
|
+
fencingToken: requiredUuid(grant, "fencingToken"),
|
|
230
|
+
allowedScopes: requiredSortedStringArray(grant, "allowedScopes"),
|
|
231
|
+
nonce: boundedRequiredString(grant, "nonce", maxGrantNonceChars),
|
|
232
|
+
expiresAt: requiredFutureContractTimestamp(grant, "expiresAt"),
|
|
233
|
+
redactionStatus,
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function parseRequestedCapabilities(value, capability) {
|
|
238
|
+
if (value === undefined || value === null) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
if (!Array.isArray(value)) {
|
|
242
|
+
throw new Error("requestedCapabilities must be an array when provided.");
|
|
243
|
+
}
|
|
244
|
+
return value.map((entry, index) => {
|
|
245
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
246
|
+
throw new Error(`requestedCapabilities[${index}] must be a non-empty string.`);
|
|
247
|
+
}
|
|
248
|
+
const requestedCapability = entry.trim().toLowerCase();
|
|
249
|
+
if (!supportedRequestedCapabilities.has(requestedCapability)) {
|
|
250
|
+
throw new Error(`Bridge Chat v1 does not support requestedCapabilities ${requestedCapability}.`);
|
|
251
|
+
}
|
|
252
|
+
if (requestedCapability === "attachment_handoffs" && !capability.supportsAttachments) {
|
|
253
|
+
throw new Error("Bridge Chat v1 does not support requestedCapabilities attachment_handoffs for this runner.");
|
|
254
|
+
}
|
|
255
|
+
if (requestedCapability === "action_broker_grants" && !capability.supportsActionBrokerGrants) {
|
|
256
|
+
throw new Error("Bridge Chat v1 does not support requestedCapabilities action_broker_grants for this runner.");
|
|
257
|
+
}
|
|
258
|
+
return requestedCapability;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function assertBoundedEnvelopesMatchFence(attachmentHandoffs, actionBrokerGrants, turnId, leaseId, fencingToken) {
|
|
262
|
+
attachmentHandoffs.forEach((handoff, index) => {
|
|
263
|
+
if (handoff.turnId !== turnId) {
|
|
264
|
+
throw new Error(`attachmentHandoffs[${index}].turnId must match turnId.`);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
actionBrokerGrants.forEach((grant, index) => {
|
|
268
|
+
if (grant.turnId !== turnId || grant.leaseId !== leaseId || grant.fencingToken !== fencingToken) {
|
|
269
|
+
throw new Error(`actionBrokerGrants[${index}] must match the active dispatch fence.`);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function assertReservedFieldsAbsent(source, label) {
|
|
274
|
+
for (const key of ["attachments", "attachmentHandles", "actionGrants"]) {
|
|
275
|
+
if (source[key] !== undefined && source[key] !== null) {
|
|
276
|
+
throw new Error(`${label}.${key} is reserved for a later Bridge Chat protocol.`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function assertOnlyKeys(source, label, allowedKeys) {
|
|
281
|
+
const allowed = new Set(allowedKeys);
|
|
282
|
+
for (const key of Object.keys(source)) {
|
|
283
|
+
if (!allowed.has(key)) {
|
|
284
|
+
throw new Error(`${label}.${key} is not supported by Bridge Chat v1.`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function asObject(value, label) {
|
|
289
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
290
|
+
throw new Error(`${label} must be an object.`);
|
|
291
|
+
}
|
|
292
|
+
return value;
|
|
293
|
+
}
|
|
294
|
+
function optionalObject(value, label) {
|
|
295
|
+
if (value === undefined || value === null) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
return asObject(value, label);
|
|
299
|
+
}
|
|
300
|
+
function requiredUuid(source, key) {
|
|
301
|
+
const value = requiredString(source, key).trim();
|
|
302
|
+
if (!uuidPattern.test(value)) {
|
|
303
|
+
throw new Error(`${key} must be a UUID.`);
|
|
304
|
+
}
|
|
305
|
+
return value;
|
|
306
|
+
}
|
|
307
|
+
function requiredString(source, key) {
|
|
308
|
+
const value = source[key];
|
|
309
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
310
|
+
throw new Error(`${key} must be a non-empty string.`);
|
|
311
|
+
}
|
|
312
|
+
return value;
|
|
313
|
+
}
|
|
314
|
+
function boundedRequiredString(source, key, maxLength) {
|
|
315
|
+
const clean = requiredString(source, key).trim();
|
|
316
|
+
if (clean.length > maxLength) {
|
|
317
|
+
throw new Error(`${key} exceeds ${maxLength} characters.`);
|
|
318
|
+
}
|
|
319
|
+
return clean;
|
|
320
|
+
}
|
|
321
|
+
function requiredNonNegativeInteger(source, key) {
|
|
322
|
+
const value = source[key];
|
|
323
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
324
|
+
throw new Error(`${key} must be a non-negative integer.`);
|
|
325
|
+
}
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
function requiredSortedStringArray(source, key) {
|
|
329
|
+
const value = source[key];
|
|
330
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
331
|
+
throw new Error(`${key} must be a non-empty string array.`);
|
|
332
|
+
}
|
|
333
|
+
const cleaned = value.map((entry, index) => {
|
|
334
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
335
|
+
throw new Error(`${key}[${index}] must be a non-empty string.`);
|
|
336
|
+
}
|
|
337
|
+
return entry.trim();
|
|
338
|
+
});
|
|
339
|
+
if (new Set(cleaned).size !== cleaned.length) {
|
|
340
|
+
throw new Error(`${key} must not contain duplicate values.`);
|
|
341
|
+
}
|
|
342
|
+
const sorted = [...cleaned].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
343
|
+
if (cleaned.some((scope, index) => scope !== sorted[index])) {
|
|
344
|
+
throw new Error(`${key} must be sorted.`);
|
|
345
|
+
}
|
|
346
|
+
return cleaned;
|
|
347
|
+
}
|
|
348
|
+
function requiredFutureContractTimestamp(source, key) {
|
|
349
|
+
const value = requiredString(source, key).trim();
|
|
350
|
+
const parsed = Date.parse(value);
|
|
351
|
+
if (!Number.isFinite(parsed) || parsed <= unixEpochMs) {
|
|
352
|
+
throw new Error(`${key} is required.`);
|
|
353
|
+
}
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
function optionalString(source, key, maxLength) {
|
|
357
|
+
const value = source[key];
|
|
358
|
+
if (value === undefined || value === null) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
if (typeof value !== "string") {
|
|
362
|
+
throw new Error(`${key} must be a string when provided.`);
|
|
363
|
+
}
|
|
364
|
+
const clean = value.trim();
|
|
365
|
+
if (clean.length === 0) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
if (maxLength !== undefined && clean.length > maxLength) {
|
|
369
|
+
throw new Error(`${key} exceeds maxProviderThreadIdChars.`);
|
|
370
|
+
}
|
|
371
|
+
return clean;
|
|
372
|
+
}
|
|
373
|
+
function cleanOptionalString(value) {
|
|
374
|
+
if (typeof value !== "string") {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
const clean = value.trim();
|
|
378
|
+
return clean.length === 0 ? null : clean;
|
|
379
|
+
}
|
|
380
|
+
function nonEmptyStringOr(value, fallback) {
|
|
381
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
|
|
382
|
+
}
|
|
383
|
+
function cleanStringArray(values, lower) {
|
|
384
|
+
const seen = new Set();
|
|
385
|
+
const cleaned = [];
|
|
386
|
+
for (const value of values) {
|
|
387
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const clean = lower ? value.trim().toLowerCase() : value.trim();
|
|
391
|
+
const key = clean.toLowerCase();
|
|
392
|
+
if (!seen.has(key)) {
|
|
393
|
+
seen.add(key);
|
|
394
|
+
cleaned.push(clean);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return cleaned;
|
|
398
|
+
}
|
|
399
|
+
function nonNegativeInteger(value) {
|
|
400
|
+
return Number.isSafeInteger(value) && value > 0 ? value : 0;
|
|
401
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BridgeCancelHandler, BridgeDispatchHandler, BridgeLeaseUpdateHandler, BridgeOfferHandler, BridgeReadinessProbeHandler, BridgeReapHandler, BridgeServiceConfig, BridgeStartHandler } from "./bridge.types.js";
|
|
1
|
+
import type { BridgeCancelHandler, BridgeChatTurnHandler, BridgeDispatchHandler, BridgeLeaseUpdateHandler, BridgeOfferHandler, BridgeReadinessProbeHandler, BridgeReapHandler, BridgeServiceConfig, BridgeStartHandler } from "./bridge.types.js";
|
|
2
2
|
export interface BridgeService {
|
|
3
3
|
connectOnce(): Promise<void>;
|
|
4
4
|
run(signal?: AbortSignal): Promise<void>;
|
|
@@ -8,7 +8,10 @@ export interface BridgeConnection {
|
|
|
8
8
|
start(): Promise<void>;
|
|
9
9
|
stop(): Promise<void>;
|
|
10
10
|
onClose(handler: (error?: Error) => void): void;
|
|
11
|
+
onReconnecting?(handler: (error?: Error) => void): void;
|
|
12
|
+
onReconnected?(handler: (connectionId?: string) => void | Promise<void>): void;
|
|
11
13
|
onDispatch(handler: BridgeDispatchHandler): void;
|
|
14
|
+
onChatTurn?(handler: BridgeChatTurnHandler): void;
|
|
12
15
|
onOffer?(handler: BridgeOfferHandler): void;
|
|
13
16
|
onStart?(handler: BridgeStartHandler): void;
|
|
14
17
|
onLeaseUpdate?(handler: BridgeLeaseUpdateHandler): void;
|