@defrex/autobuild 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
# Remote BuildStore protocol
|
|
2
|
+
|
|
3
|
+
This document is the complete normative HTTP contract for Autobuild's remote
|
|
4
|
+
`BuildStore`. Implement this protocol to provide build storage on any database,
|
|
5
|
+
blob store, or service stack; Autobuild does not load in-process `BuildStore`
|
|
6
|
+
plugins. Compatibility means that an Autobuild remote client can use the server
|
|
7
|
+
and the implementation passes the shared `BuildStore` contract exported by
|
|
8
|
+
`@defrex/autobuild/plugin-sdk`.
|
|
9
|
+
|
|
10
|
+
The transport, wire shapes, validation order, atomicity, lease behavior,
|
|
11
|
+
persistence requirements, and conformance bar for the protocol shipped in this
|
|
12
|
+
distribution are all defined here. A protocol implementation must target the
|
|
13
|
+
same Autobuild package version used by its clients; protocol changes update this
|
|
14
|
+
document and the package's executable contracts together.
|
|
15
|
+
|
|
16
|
+
## 1. Transport and conventions
|
|
17
|
+
|
|
18
|
+
- The transport is JSON over HTTP or HTTPS. There is no API version prefix.
|
|
19
|
+
Point Autobuild at the server's base URL with `--store`, or set that URL in
|
|
20
|
+
`AB_STORE`; the shipped client appends the routes below.
|
|
21
|
+
- Every `/builds`, `/repos`, and `/sessions` request sends
|
|
22
|
+
`X-Autobuild-Version` with the exact client package version and
|
|
23
|
+
`X-Autobuild-Protocol-Version` with the remote protocol version (`2` in this
|
|
24
|
+
distribution). Before authentication,
|
|
25
|
+
body parsing, or resource lookup, the server compares both exact strings.
|
|
26
|
+
Missing or different values receive `409 {"kind":"conflict","error":"…"}`;
|
|
27
|
+
the diagnostic names the client and server package and protocol versions.
|
|
28
|
+
- Clients send `Content-Type: application/json` for requests with bodies. Every
|
|
29
|
+
response, including errors and `null` artifact results, has
|
|
30
|
+
`Content-Type: application/json`.
|
|
31
|
+
- Build slugs, repository ids, artifact kinds, and holders are case-sensitive.
|
|
32
|
+
- A build slug or repository id in a path is one percent-encoded path segment.
|
|
33
|
+
In particular, a repository id such as `acme/widgets` is sent as
|
|
34
|
+
`acme%2Fwidgets`, not as two segments.
|
|
35
|
+
- Artifact bytes are represented by `contentBase64`, using strict, canonical,
|
|
36
|
+
padded standard base64 (not base64url). A server may impose a documented
|
|
37
|
+
decoded-byte ceiling. It decodes and checks every standalone or bundled
|
|
38
|
+
artifact before any backing mutation; excess content receives `413
|
|
39
|
+
{"kind":"validation","error":"…"}` naming the byte ceiling. The shipped
|
|
40
|
+
hosted service ceiling is 1,048,576 decoded bytes (1 MiB).
|
|
41
|
+
- Store-assigned times are ISO-8601 UTC strings in JavaScript
|
|
42
|
+
`Date.toISOString()` form, for example `2026-07-15T12:00:00.000Z`.
|
|
43
|
+
- Event sequence numbers and artifact revisions are integers assigned by the
|
|
44
|
+
server. Clients must not assign either except for the negative deposit
|
|
45
|
+
placeholders described in [Atomic deposits](#7-atomic-deposits).
|
|
46
|
+
- Optional object members are omitted, not represented as `null`, unless a
|
|
47
|
+
response below explicitly specifies JSON `null`.
|
|
48
|
+
- The server owns durability. Events and artifact metadata visible in a
|
|
49
|
+
successful response must remain available across server restarts.
|
|
50
|
+
|
|
51
|
+
## 2. Common wire types
|
|
52
|
+
|
|
53
|
+
The examples in this section use TypeScript-like optional markers and JSON
|
|
54
|
+
comments for compactness. Actual messages are JSON.
|
|
55
|
+
|
|
56
|
+
### Ticket and records
|
|
57
|
+
|
|
58
|
+
```jsonc
|
|
59
|
+
// TicketRef
|
|
60
|
+
{
|
|
61
|
+
"source": "linear", // nonempty string
|
|
62
|
+
"id": "AUT-123", // nonempty string
|
|
63
|
+
"url": "https://…", // optional string
|
|
64
|
+
"title": "Document the store" // optional string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// BuildRecord
|
|
68
|
+
{
|
|
69
|
+
"slug": "remote-store-protocol",
|
|
70
|
+
"repo": "acme/autobuild",
|
|
71
|
+
"repoOrigin": "https://github.com/acme/autobuild", // optional
|
|
72
|
+
"ticket": { /* TicketRef */ }, // optional
|
|
73
|
+
"branch": "ab/remote-store-protocol", // optional
|
|
74
|
+
"createdAt": "2026-07-15T12:00:00.000Z",
|
|
75
|
+
"updatedAt": "2026-07-15T12:01:00.000Z",
|
|
76
|
+
"lease": { // optional
|
|
77
|
+
"holder": "runner-1",
|
|
78
|
+
"expiresAt": "2026-07-15T12:02:00.000Z"
|
|
79
|
+
},
|
|
80
|
+
"heartbeatAt": "2026-07-15T12:01:30.000Z" // optional
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// RepositoryRecord
|
|
84
|
+
{
|
|
85
|
+
"repo": "acme/autobuild",
|
|
86
|
+
"createdAt": "2026-07-15T12:00:00.000Z",
|
|
87
|
+
"updatedAt": "2026-07-15T12:01:00.000Z",
|
|
88
|
+
"lease": { // optional
|
|
89
|
+
"holder": "harvest-1",
|
|
90
|
+
"expiresAt": "2026-07-15T12:02:00.000Z"
|
|
91
|
+
},
|
|
92
|
+
"heartbeatAt": "2026-07-15T12:01:30.000Z" // optional
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Build records do not contain derived build status. Status is reduced from the
|
|
97
|
+
build event stream. `createdAt`, `updatedAt`, lease expiry, and heartbeat time
|
|
98
|
+
are server-owned.
|
|
99
|
+
|
|
100
|
+
`repoOrigin` is the build repository's normalized git origin URL (scp-like and
|
|
101
|
+
ssh remotes mapped to their `https://` spelling, host lowercased, trailing
|
|
102
|
+
`.git` and slashes stripped). It is a location-independent secondary identity:
|
|
103
|
+
clients may accept a differently located checkout of the same repository by
|
|
104
|
+
origin equality. It is optional on both create and record shapes, and the
|
|
105
|
+
field is simply absent when the repository has no origin remote. The field is
|
|
106
|
+
an additive, optional member: servers that predate it strip it from create
|
|
107
|
+
requests (clients fall back to path-only identity) and never return it, and
|
|
108
|
+
clients that predate it never send or expect it — so neither side requires a
|
|
109
|
+
protocol-version bump.
|
|
110
|
+
|
|
111
|
+
### Actors, event writes, and envelopes
|
|
112
|
+
|
|
113
|
+
An actor is exactly one of:
|
|
114
|
+
|
|
115
|
+
```jsonc
|
|
116
|
+
{ "kind": "kernel" }
|
|
117
|
+
{ "kind": "agent", "role": "implement", "session": "s_123" }
|
|
118
|
+
{ "kind": "human", "user": "alice" }
|
|
119
|
+
{ "kind": "human", "user": "alice", "via": { "kind": "session", "id": "os_123" } }
|
|
120
|
+
{ "kind": "human", "user": "alice", "via": { "kind": "mcp", "client": "claude-code" } }
|
|
121
|
+
{ "kind": "dispatcher" }
|
|
122
|
+
{ "kind": "ingester", "source": "sentry" }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`role`, `session`, `user`, and `source` are nonempty strings. A human actor may
|
|
126
|
+
carry an optional `via` marker naming the delegate that performed the write on
|
|
127
|
+
the person's behalf: `{ "kind": "session", "id": nonempty }` (an operator's
|
|
128
|
+
orchestrator session) or `{ "kind": "mcp", "client": nonempty }` (an MCP
|
|
129
|
+
client). `via` is valid only on `human` actors and only on build and
|
|
130
|
+
repository events — session events are the delegate's own log — and a `via`
|
|
131
|
+
key on any other actor variant is a validation failure. These are the
|
|
132
|
+
complete actor variants; unknown actor kinds or extra fields are invalid.
|
|
133
|
+
|
|
134
|
+
An event append request contains no resource id, sequence, or timestamp:
|
|
135
|
+
|
|
136
|
+
```jsonc
|
|
137
|
+
{
|
|
138
|
+
"actor": { /* Actor */ },
|
|
139
|
+
"type": "observation.recorded", // nonempty string at the wire boundary
|
|
140
|
+
"payload": { /* type-specific JSON */ }
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
A successful build event response is:
|
|
145
|
+
|
|
146
|
+
```jsonc
|
|
147
|
+
{
|
|
148
|
+
"build": "remote-store-protocol",
|
|
149
|
+
"seq": 1, // positive integer
|
|
150
|
+
"ts": "2026-07-15T12:00:00.000Z",
|
|
151
|
+
"actor": { /* Actor */ },
|
|
152
|
+
"type": "observation.recorded",
|
|
153
|
+
"payload": { /* validated payload */ }
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A repository event envelope has the same shape with `"repo"` in place of
|
|
158
|
+
`"build"`. Event-list responses are JSON arrays of the corresponding
|
|
159
|
+
envelopes.
|
|
160
|
+
|
|
161
|
+
A conditional build-stream append request wraps the same event write with the
|
|
162
|
+
currently observed sequence (`0` means an empty stream):
|
|
163
|
+
|
|
164
|
+
```jsonc
|
|
165
|
+
{
|
|
166
|
+
"expectedSeq": 17, // nonnegative integer
|
|
167
|
+
"event": { /* event write */ }
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Its response is either the appended build event envelope or JSON `null` when
|
|
172
|
+
the stream no longer has that sequence.
|
|
173
|
+
|
|
174
|
+
The wire accepts the generic event-write shape so that the backing server can
|
|
175
|
+
return a precise catalog validation error. A conforming server **must** validate
|
|
176
|
+
the complete actor, type, payload, and actor-per-type rule before mutation.
|
|
177
|
+
Build, repository, and session events have separate catalogs; each catalog
|
|
178
|
+
defines its allowed event names, exact payload shape, and allowed actor kinds.
|
|
179
|
+
Unknown types, missing or extra payload fields, malformed artifact references,
|
|
180
|
+
and an actor kind not authorized for that event are validation failures. A
|
|
181
|
+
`via` member on any non-human actor is a validation failure with an explicit
|
|
182
|
+
rule message (see [Operator-session operations](#5-operator-session-operations)
|
|
183
|
+
and section 7).
|
|
184
|
+
|
|
185
|
+
Those event catalogs evolve with Autobuild's build lifecycle rather than with
|
|
186
|
+
the storage transport. A server therefore must use or faithfully implement the
|
|
187
|
+
catalog from the same Autobuild package version as its client. The public
|
|
188
|
+
`BuildStore` type (including its typed write methods), representative valid
|
|
189
|
+
writes, and unchanged contract suite exported by `@defrex/autobuild/plugin-sdk` are the
|
|
190
|
+
package-versioned compatibility surface. Wire validation remains mandatory even
|
|
191
|
+
in a language that cannot consume those TypeScript types directly.
|
|
192
|
+
|
|
193
|
+
### Artifacts
|
|
194
|
+
|
|
195
|
+
A build artifact input is:
|
|
196
|
+
|
|
197
|
+
```jsonc
|
|
198
|
+
{
|
|
199
|
+
"kind": "plan", // nonempty string
|
|
200
|
+
"contentBase64": "IyBQbGFuCg==", // standard base64
|
|
201
|
+
"metadata": { "round": 1 } // optional JSON object; defaults to {}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
A build artifact metadata response is:
|
|
206
|
+
|
|
207
|
+
```jsonc
|
|
208
|
+
{
|
|
209
|
+
"build": "remote-store-protocol",
|
|
210
|
+
"kind": "plan",
|
|
211
|
+
"revision": 0, // nonnegative integer
|
|
212
|
+
"blobRef": "64-lowercase-hex-sha256",
|
|
213
|
+
"metadata": { "round": 1 },
|
|
214
|
+
"createdAt": "2026-07-15T12:00:00.000Z"
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Repository artifact metadata has `"repo"` instead of `"build"`. An artifact
|
|
219
|
+
read returns either JSON `null` or:
|
|
220
|
+
|
|
221
|
+
```jsonc
|
|
222
|
+
{
|
|
223
|
+
"meta": { /* build or repository artifact metadata */ },
|
|
224
|
+
"contentBase64": "IyBQbGFuCg=="
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`blobRef` is the lowercase hexadecimal SHA-256 digest of the decoded bytes.
|
|
229
|
+
Revisions start at 0 independently for each artifact kind. Depositing identical
|
|
230
|
+
bytes again may reuse the blob but still creates the next metadata revision.
|
|
231
|
+
|
|
232
|
+
### Atomic deposit messages
|
|
233
|
+
|
|
234
|
+
Both resource families use this request shape:
|
|
235
|
+
|
|
236
|
+
```jsonc
|
|
237
|
+
{
|
|
238
|
+
"artifacts": [
|
|
239
|
+
{ "kind": "plan", "contentBase64": "…", "metadata": { "round": 1 } }
|
|
240
|
+
],
|
|
241
|
+
"event": {
|
|
242
|
+
"actor": { /* Actor */ },
|
|
243
|
+
"type": "plan.completed",
|
|
244
|
+
"payload": {
|
|
245
|
+
"artifact": { "kind": "plan", "rev": -1 }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The response is:
|
|
252
|
+
|
|
253
|
+
```jsonc
|
|
254
|
+
{
|
|
255
|
+
"event": { /* build or repository event envelope */ },
|
|
256
|
+
"artifacts": [ /* assigned metadata, in request order */ ]
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Section 7 defines the placeholder and transaction semantics.
|
|
261
|
+
|
|
262
|
+
### Lease and error messages
|
|
263
|
+
|
|
264
|
+
```jsonc
|
|
265
|
+
// claim request
|
|
266
|
+
{ "holder": "runner-1", "ttlMs": 60000 }
|
|
267
|
+
|
|
268
|
+
// heartbeat or release request
|
|
269
|
+
{ "holder": "runner-1" }
|
|
270
|
+
|
|
271
|
+
// every lease response
|
|
272
|
+
{ "ok": true }
|
|
273
|
+
|
|
274
|
+
// every non-success response
|
|
275
|
+
{
|
|
276
|
+
"error": "human-readable message",
|
|
277
|
+
"kind": "validation" // validation | not-found | auth | conflict | internal
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`holder` is nonempty. `ttlMs` is a nonnegative integer measured in
|
|
282
|
+
milliseconds. A zero TTL is valid on the wire and produces an immediately
|
|
283
|
+
expired lease.
|
|
284
|
+
|
|
285
|
+
## 3. Build-stream operations
|
|
286
|
+
|
|
287
|
+
`{slug}` means one percent-encoded build slug. Except for `POST /builds` and
|
|
288
|
+
`GET /builds`, all routes first authorize the addressed build and then require
|
|
289
|
+
it to exist. An unknown build returns `404 not-found`.
|
|
290
|
+
|
|
291
|
+
| BuildStore operation | HTTP route | Request | Success |
|
|
292
|
+
|---|---|---|---|
|
|
293
|
+
| `createBuild` | `POST /builds` | `{slug: string, repo: string, repoOrigin?: string, ticket?: TicketRef, branch?: string}`; `slug`, `repo`, and a supplied `branch` are nonempty | `201` + `BuildRecord`; duplicate slug is `409 conflict` |
|
|
294
|
+
| `listBuilds` | `GET /builds` | none | `200` + `BuildRecord[]`; list order is unspecified |
|
|
295
|
+
| `getBuild` | `GET /builds/{slug}` | none | `200` + `BuildRecord`; absent is `404` (the shipped client maps this to `null`) |
|
|
296
|
+
| `append` | `POST /builds/{slug}/events` | event write | `201` + build event envelope |
|
|
297
|
+
| `appendIfCurrent` | `POST /builds/{slug}/events/conditional` | `{"expectedSeq": nonnegative integer, "event": event write}` | appended: `201` + build event envelope; stream advanced: `200 null` |
|
|
298
|
+
| `getEvents` | `GET /builds/{slug}/events?since={n}&wait={s}` | optional `since` query value, parsed below; absence defaults to `0`. Optional `wait` in whole seconds, parsed per the event-wait rules below | `200` + envelopes whose `seq` is strictly greater than parsed `since`, in increasing sequence order |
|
|
299
|
+
| `appendWithArtifacts` | `POST /builds/{slug}/deposits` | atomic deposit request | `201` + `{event, artifacts}` |
|
|
300
|
+
| `putArtifact` | `POST /builds/{slug}/artifacts` | artifact input | `201` + build artifact metadata |
|
|
301
|
+
| `getArtifact` | `GET /builds/{slug}/artifacts?kind={kind}&rev={n}` | `kind` is required and nonempty; optional `rev` is parsed below | `200` + artifact read; an absent `rev` parameter selects the latest revision; a missing kind/revision is `200 null` |
|
|
302
|
+
| `listArtifacts` | `GET /builds/{slug}/artifact-list?kind={kind}` | optional `kind`; absence means all kinds | `200` + metadata ordered by kind and then increasing revision |
|
|
303
|
+
| `claimLease` | `POST /builds/{slug}/lease/claim` | claim request | `200 {"ok": boolean}` |
|
|
304
|
+
| `heartbeat` | `POST /builds/{slug}/lease/heartbeat` | holder request | `200 {"ok": boolean}` |
|
|
305
|
+
| `releaseLease` | `POST /builds/{slug}/lease/release` | holder request | `200 {"ok": true}`; a wrong holder is a successful no-op |
|
|
306
|
+
|
|
307
|
+
The event stream is append-only. The server assigns each build's sequences
|
|
308
|
+
independently, starting at 1, and assigns the envelope timestamp. Successful
|
|
309
|
+
appends preserve append order. Event and artifact writes update the record's
|
|
310
|
+
server-owned timestamps according to the backing `BuildStore` contract.
|
|
311
|
+
|
|
312
|
+
A conditional append validates the candidate exactly like an ordinary append,
|
|
313
|
+
including actor rules, even when its expected sequence is stale. The backing
|
|
314
|
+
store must compare the stream tail and append as one atomic operation. At most
|
|
315
|
+
one caller can succeed for a given current sequence. A comparison miss returns
|
|
316
|
+
`null` and must not append, consume a sequence, or update the build record's
|
|
317
|
+
timestamps. An unknown build remains `404 not-found`; malformed or negative
|
|
318
|
+
`expectedSeq` is `400 validation`.
|
|
319
|
+
|
|
320
|
+
For a present `since` or `rev`, the shipped server applies JavaScript
|
|
321
|
+
`Number(rawValue)` and accepts the result only when `Number.isInteger` is true.
|
|
322
|
+
Consequently hexadecimal and exponent forms such as `0x10` and `1e1`,
|
|
323
|
+
whitespace-padded values, and empty or whitespace-only values are accepted;
|
|
324
|
+
empty and whitespace-only values convert to `0`. Values converting to `NaN`, infinity, or a
|
|
325
|
+
non-integral number produce `400 validation`. An absent `since` defaults to
|
|
326
|
+
`0`. An absent `rev` selects the latest artifact, but a present empty `rev=`
|
|
327
|
+
selects revision `0`. Sequence filtering is always strict `>`, so a client can
|
|
328
|
+
pass the last sequence it has processed without receiving it again. Artifact
|
|
329
|
+
revisions are nonnegative; a requested revision that does not exist, including
|
|
330
|
+
a negative one, produces `null`. A missing or empty `kind` on the artifact-read
|
|
331
|
+
route is `400 validation`.
|
|
332
|
+
|
|
333
|
+
The artifact-list `kind` is optional and is normally a nonempty stored kind.
|
|
334
|
+
No pagination, deletion, retention, range request, or streaming endpoint is
|
|
335
|
+
part of this protocol. Retention is enforced server-side inside the store at
|
|
336
|
+
deposit time (bounded pruning of dispatcher run/config artifact revisions),
|
|
337
|
+
not through any endpoint.
|
|
338
|
+
|
|
339
|
+
### The event-read bounded wait
|
|
340
|
+
|
|
341
|
+
Both event reads (`getEvents` and, symmetrically, the `getRepoEvents` read of
|
|
342
|
+
section 4) accept an optional `wait` parameter in whole seconds. When at least
|
|
343
|
+
one event with `seq` strictly greater than parsed `since` already exists, the
|
|
344
|
+
response is returned immediately, exactly as without `wait`. Otherwise the
|
|
345
|
+
server may hold the request until such an event is appended or the bound
|
|
346
|
+
elapses, whichever comes first; at the bound it answers `200 []`. Omitting
|
|
347
|
+
`wait` (or `wait=0`) is byte-identical behavior to the immediate form.
|
|
348
|
+
|
|
349
|
+
`wait` is parsed with a dedicated digits-only grammar — one or more ASCII
|
|
350
|
+
digits — deliberately stricter than the lenient `since`/`rev` conversion above:
|
|
351
|
+
empty, whitespace-padded, hexadecimal, signed, and non-integral values are `400
|
|
352
|
+
validation`, not coerced. Values above the server's documented ceiling are
|
|
353
|
+
clamped to it, never rejected; the shipped self-hosted server's ceiling is 30
|
|
354
|
+
seconds (the stream/session wait ceiling) and the shipped hosted service's is
|
|
355
|
+
25 seconds, stated in its README together with the route `maxDuration` the hold
|
|
356
|
+
requires. A held request's authentication, token scope, and version-skew rules
|
|
357
|
+
are identical to the immediate form: a `401`, `403`, or `409` identity failure
|
|
358
|
+
is answered immediately, never held.
|
|
359
|
+
|
|
360
|
+
The parameter is purely additive, so the protocol version stays `2`: a server
|
|
361
|
+
that ignores `wait` remains conforming — its immediate empty answers only raise
|
|
362
|
+
the client's request rate — and a client that sends `wait` to such a server
|
|
363
|
+
observes no other difference. This is the same reasoning the stream and session
|
|
364
|
+
waits apply. There is no push transport; a held request is the whole mechanism.
|
|
365
|
+
|
|
366
|
+
## 4. Repository-journal operations
|
|
367
|
+
|
|
368
|
+
Repository journals are separate resources with independent event sequences,
|
|
369
|
+
artifacts, and leases. `{repo}` is one percent-encoded repository id; encode
|
|
370
|
+
embedded `/` characters. All resource routes authorize and require the
|
|
371
|
+
repository to exist. An unknown repository returns `404 not-found`.
|
|
372
|
+
|
|
373
|
+
| BuildStore operation | HTTP route | Request | Success |
|
|
374
|
+
|---|---|---|---|
|
|
375
|
+
| `ensureRepo` | `POST /repos` | `{"repo": string}` with a nonempty id | `200` + `RepositoryRecord`; idempotently returns the existing record |
|
|
376
|
+
| `getRepo` | `GET /repos/{repo}` | none | `200` + `RepositoryRecord`; absent is `404` (the shipped client maps this to `null`) |
|
|
377
|
+
| `appendRepo` | `POST /repos/{repo}/events` | event write | `201` + repository event envelope |
|
|
378
|
+
| `getRepoEvents` | `GET /repos/{repo}/events?since={n}&wait={s}` | optional `since` query value, parsed as in section 3; absence defaults to `0`. Optional `wait` in whole seconds, per the event-wait rules of section 3 | `200` + envelopes with `seq >` parsed `since`, in increasing sequence order |
|
|
379
|
+
| `appendRepoWithArtifacts` | `POST /repos/{repo}/deposits` | atomic deposit request | `201` + `{event, artifacts}` using repository shapes |
|
|
380
|
+
| `putRepoArtifact` | `POST /repos/{repo}/artifacts` | artifact input | `201` + repository artifact metadata |
|
|
381
|
+
| `getRepoArtifact` | `GET /repos/{repo}/artifacts?kind={kind}&rev={n}` | required nonempty `kind`; optional `rev` is parsed as in section 3 | `200` + artifact read; latest only when the `rev` parameter is absent; missing kind/revision is `200 null` |
|
|
382
|
+
| `listRepoArtifacts` | `GET /repos/{repo}/artifact-list?kind={kind}` | optional `kind`; absence means all kinds | `200` + metadata ordered by kind and then increasing revision |
|
|
383
|
+
| `claimRepoLease` | `POST /repos/{repo}/lease/claim` | claim request | `200 {"ok": boolean}` |
|
|
384
|
+
| `heartbeatRepo` | `POST /repos/{repo}/lease/heartbeat` | holder request | `200 {"ok": boolean}` |
|
|
385
|
+
| `releaseRepoLease` | `POST /repos/{repo}/lease/release` | holder request | `200 {"ok": true}`; a wrong holder is a successful no-op |
|
|
386
|
+
|
|
387
|
+
There is no repository-list operation. `ensureRepo` is the only repository
|
|
388
|
+
creation operation and must not reset timestamps, events, artifacts, or lease
|
|
389
|
+
state when the repository already exists. Event sequence numbering starts at 1
|
|
390
|
+
for each repository independently of every build and other repository.
|
|
391
|
+
|
|
392
|
+
The build-stream query, artifact, ordering, timestamp, and validation rules
|
|
393
|
+
apply symmetrically to repository journals, including the event-read bounded
|
|
394
|
+
wait of section 3.
|
|
395
|
+
|
|
396
|
+
## 5. Operator-session operations
|
|
397
|
+
|
|
398
|
+
Operator sessions are a third resource kind: durable orchestrator-conversation
|
|
399
|
+
state keyed by repository and operator. They are hosted-only — local installs
|
|
400
|
+
interact through the CLI and dashboard and never create one — but the resource
|
|
401
|
+
contract is uniform, and a conforming server implements it the same way it
|
|
402
|
+
implements the repository journal. A session record is:
|
|
403
|
+
|
|
404
|
+
```jsonc
|
|
405
|
+
// SessionRecord
|
|
406
|
+
{
|
|
407
|
+
"id": "os_9f2c…", // server-assigned, `os_`-prefixed
|
|
408
|
+
"repo": "acme/autobuild",
|
|
409
|
+
"operator": "alice", // trimmed nonblank operator identity
|
|
410
|
+
"title": "fix the login flow", // optional
|
|
411
|
+
"createdAt": "2026-07-15T12:00:00.000Z",
|
|
412
|
+
"updatedAt": "2026-07-15T12:03:00.000Z"
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Creating a session makes the record and its first event visible together: the
|
|
417
|
+
record's creation atomically appends a `session.created` event as sequence 1
|
|
418
|
+
with the operator as a human actor. The session event catalog is closed and
|
|
419
|
+
separate from the build and repository catalogs (`session.created`,
|
|
420
|
+
`message.posted`, `session.wake-set`, `turn.started`, `turn.suspended`,
|
|
421
|
+
`turn.resumed`, `approval.requested`, `approval.answered`, `turn.completed`,
|
|
422
|
+
`turn.failed`, `session.archived`), with its own per-session sequence starting
|
|
423
|
+
at 1 and its own actor-per-type rules. A `via` member is never valid on a
|
|
424
|
+
session event: session events are the delegate's own log, and the operator's
|
|
425
|
+
identity is already the actor.
|
|
426
|
+
|
|
427
|
+
| BuildStore operation | HTTP route | Request | Success |
|
|
428
|
+
|---|---|---|---|
|
|
429
|
+
| `createSession` | `POST /repos/{repo}/sessions` | `{"repo": string, "operator": string, "title"?: string}`; the body `repo` must equal the path repository | `201` + `SessionRecord` |
|
|
430
|
+
| `listSessions` | `GET /repos/{repo}/sessions` | none | `200` + `SessionRecord[]`, creation order (`createdAt`, then the store's creation counter for same-millisecond ties) |
|
|
431
|
+
| `getSession` | `GET /sessions/{id}` | none | `200` + `SessionRecord`; absent is `404` (the shipped client maps this to `null`) |
|
|
432
|
+
| `appendSessionEvent` | `POST /sessions/{id}/events` | event write | `201` + session event envelope (same envelope shape with `"session"` in place of `"build"`) |
|
|
433
|
+
| `getSessionEvents` | `GET /sessions/{id}/events?since={n}&wait={s}` | optional `since` query value, parsed as in section 3; absence defaults to `0`. Optional `wait` in whole seconds, per the event-wait rules of section 3 | `200` + session event envelopes with `seq >` parsed `since`, in increasing sequence order |
|
|
434
|
+
| `appendSessionWithArtifacts` | `POST /sessions/{id}/deposits` | atomic deposit request | `201` + `{event, artifacts}` using session shapes; the substitution algorithm of section 8 applies unchanged |
|
|
435
|
+
| `putSessionArtifact` | `POST /sessions/{id}/artifacts` | artifact input | `201` + session artifact metadata |
|
|
436
|
+
| `getSessionArtifact` | `GET /sessions/{id}/artifacts?kind={kind}&rev={n}` | required nonempty `kind`; optional `rev` | `200` + artifact read; missing kind/revision is `200 null` |
|
|
437
|
+
| `listSessionArtifacts` | `GET /sessions/{id}/artifact-list?kind={kind}` | optional `kind` | `200` + metadata ordered by kind and then increasing revision |
|
|
438
|
+
|
|
439
|
+
Session-scoped streams: the stream scope vocabulary gains
|
|
440
|
+
`{ "kind": "session", "session": "os_…" }`. `createStream` and `listStreams`
|
|
441
|
+
exist under the session family (`POST|GET /sessions/{id}/streams`), the
|
|
442
|
+
addressed chunk/close operations work unchanged, and a close deposits its
|
|
443
|
+
finalized artifact on the session's artifact table. All stream rules of
|
|
444
|
+
section 6 apply to session-scoped streams unchanged.
|
|
445
|
+
|
|
446
|
+
Event reads honor the bounded wait: when no newer event exists, the server may
|
|
447
|
+
hold the request up to the parsed `wait` bound in whole seconds, returning as
|
|
448
|
+
soon as an event is appended and no later than the bound; a `wait` above the
|
|
449
|
+
event-wait ceiling of section 3 — 30 seconds on the shipped self-hosted server,
|
|
450
|
+
25 on the shipped hosted service — is clamped to it. Held session-event reads poll at the hosted
|
|
451
|
+
one-second budget (`EVENT_WAIT_POLL_MS = 1000`), matching the build and
|
|
452
|
+
repository event reads, so append-to-wake is typically under one second. The
|
|
453
|
+
clamp and early-return semantics are exactly the
|
|
454
|
+
stream read's, so a poll loop cannot drift between the two.
|
|
455
|
+
|
|
456
|
+
Deposits, artifacts, and per-session sequencing follow the same contracts as
|
|
457
|
+
the repository journal: the atomic-deposit guarantee of section 8, latest or
|
|
458
|
+
pinned artifact reads, and independent per-session sequence numbering. There
|
|
459
|
+
is no session lease family.
|
|
460
|
+
|
|
461
|
+
## 6. Stream operations
|
|
462
|
+
|
|
463
|
+
Streams are the BuildStore's third primitive: an append-only, per-stream
|
|
464
|
+
sequenced log of chunks with an open-then-closed lifecycle that finalizes into
|
|
465
|
+
an artifact. A chunk is a nonempty batch of protocol parts from the Vercel AI
|
|
466
|
+
SDK UI Message Stream protocol, version 1 of the current major (AI SDK 7):
|
|
467
|
+
each part is a JSON object with a nonempty-string `type`, and part types are
|
|
468
|
+
exactly that protocol's (`text-start`/`text-delta`/`text-end`,
|
|
469
|
+
`reasoning-start`/`-delta`/`-end`, `tool-input-*`, `tool-output-*`,
|
|
470
|
+
`tool-approval-*`, `start-step`, `finish-step`, `start`, `finish`, `abort`,
|
|
471
|
+
`error`, `message-metadata`, `source-url`, `source-document`, `file`,
|
|
472
|
+
`reasoning-file`, `custom`, and the open `data-*` namespace). The store
|
|
473
|
+
performs no protocol validation on append; validation happens once, at close,
|
|
474
|
+
when the chunks assemble into the protocol's `UIMessage[]` document. Streams
|
|
475
|
+
are presentation, never routing: no kernel, engine, reducer, or dispatcher
|
|
476
|
+
decision reads stream content.
|
|
477
|
+
|
|
478
|
+
Every stream record carries a server-assigned id (`st_`-prefixed), its scope,
|
|
479
|
+
a caller-supplied label, the literal format `ai-ui-message-stream/v1`, a
|
|
480
|
+
status of `open` or `closed`, `createdAt`, and, once closed, its outcome
|
|
481
|
+
(`completed` or `aborted`), `closedAt`, and the reference of its finalized
|
|
482
|
+
artifact:
|
|
483
|
+
|
|
484
|
+
```jsonc
|
|
485
|
+
// StreamRecord
|
|
486
|
+
{
|
|
487
|
+
"id": "st_9f2c…",
|
|
488
|
+
"scope": { "kind": "build", "build": "remote-store-protocol" },
|
|
489
|
+
// or { "kind": "repo", "repo": "acme/autobuild" }
|
|
490
|
+
"label": "implement round 1",
|
|
491
|
+
"format": "ai-ui-message-stream/v1",
|
|
492
|
+
"status": "closed",
|
|
493
|
+
"createdAt": "2026-07-15T12:00:00.000Z",
|
|
494
|
+
"closedAt": "2026-07-15T12:03:00.000Z", // optional; closed only
|
|
495
|
+
"outcome": "completed", // optional; closed only
|
|
496
|
+
"artifact": { // optional; closed only
|
|
497
|
+
"kind": "stream:st_9f2c…",
|
|
498
|
+
"revision": 0,
|
|
499
|
+
"blobRef": "64-lowercase-hex-sha256"
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
A chunk response is:
|
|
505
|
+
|
|
506
|
+
```jsonc
|
|
507
|
+
{
|
|
508
|
+
"stream": "st_9f2c…",
|
|
509
|
+
"seq": 1, // per-stream, assigned by the server from 1
|
|
510
|
+
"ts": "2026-07-15T12:00:00.000Z",
|
|
511
|
+
"parts": [ { "type": "text-delta", "id": "t1", "delta": "…" } ]
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
A read response carries the chunks plus the stream's current status and,
|
|
516
|
+
when closed, its outcome and artifact reference:
|
|
517
|
+
|
|
518
|
+
```jsonc
|
|
519
|
+
{
|
|
520
|
+
"chunks": [ /* StreamChunk */ ],
|
|
521
|
+
"status": "open",
|
|
522
|
+
"outcome": "completed", // optional; closed only
|
|
523
|
+
"artifact": { /* StreamArtifactRef */ } // optional; closed only
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
`{slug}` and `{repo}` mean the same percent-encoded path segments as above.
|
|
528
|
+
All six operations exist under both resource families; the scope is fixed at
|
|
529
|
+
create and comes from the path. All stream routes authorize like the event
|
|
530
|
+
routes (section 7: resource scope gates everything) and carry no
|
|
531
|
+
session-attribution dimension — stream parts have no actor.
|
|
532
|
+
|
|
533
|
+
| BuildStore operation | HTTP route | Request | Success |
|
|
534
|
+
|---|---|---|---|
|
|
535
|
+
| `createStream` | `POST /builds/{slug}/streams` and `POST /repos/{repo}/streams` | `{"label": string}` with a nonempty label | `201` + `StreamRecord` |
|
|
536
|
+
| `listStreams` | `GET /builds/{slug}/streams` and `GET /repos/{repo}/streams` | none | `200` + `StreamRecord[]`, creation order (`createdAt`, then the store's creation counter for same-millisecond ties) |
|
|
537
|
+
| `getStream` | `GET /builds/{slug}/streams/{id}` and `GET /repos/{repo}/streams/{id}` | none | `200` + `StreamRecord`; `404` when unknown **or** scoped to another resource |
|
|
538
|
+
| `appendStreamParts` | `POST /builds/{slug}/streams/{id}/chunks` and `POST /repos/{repo}/streams/{id}/chunks` | `{"parts": [ { "type": nonempty string, … } ]}`, nonempty | `201` + `StreamChunk` |
|
|
539
|
+
| `readStream` | `GET /builds/{slug}/streams/{id}/chunks?since={n}&wait={n}` and the `/repos/{repo}` form | optional `since` (default `0`) parsed leniently like the `since` of section 3; `wait` (whole seconds) parsed by the digits-only grammar of section 3's event-wait rules — one or more ASCII digits, anything else `400 validation` per section 9 — and clamped to the 30-second stream ceiling below | `200` + read response: chunks with `seq >` parsed `since`, in increasing order |
|
|
540
|
+
| `closeStream` | `POST /builds/{slug}/streams/{id}/close` and `POST /repos/{repo}/streams/{id}/close` | `{"outcome": "completed" \| "aborted"}` | `200` + the closed `StreamRecord` |
|
|
541
|
+
|
|
542
|
+
Because a stream id is globally unique but names no scope, the shipped server
|
|
543
|
+
and client additionally expose the four addressed operations as top-level
|
|
544
|
+
routes, which resolve the stream's own scope and then authorize against it:
|
|
545
|
+
|
|
546
|
+
| Operation | Top-level route | Notes |
|
|
547
|
+
|---|---|---|
|
|
548
|
+
| `getStream` | `GET /streams/{id}` | `404` maps to `null` in the shipped client |
|
|
549
|
+
| `appendStreamParts` | `POST /streams/{id}/chunks` | |
|
|
550
|
+
| `readStream` | `GET /streams/{id}/chunks?since={n}&wait={n}` | |
|
|
551
|
+
| `closeStream` | `POST /streams/{id}/close` | |
|
|
552
|
+
|
|
553
|
+
On both route families, a stream that does not exist and one scoped to a
|
|
554
|
+
different resource are the same `404 not-found` (`unknown stream "…"`), so no
|
|
555
|
+
cross-scope existence leaks; a token that does not cover the resolved scope
|
|
556
|
+
receives `401`/`403` per section 7.
|
|
557
|
+
|
|
558
|
+
Append semantics, enforced before any mutation:
|
|
559
|
+
|
|
560
|
+
- Each part must be a JSON object whose `type` is a nonempty string; anything
|
|
561
|
+
else is `400 validation`. Unknown part keys must survive the wire untouched.
|
|
562
|
+
- The server assigns each appended batch a per-stream sequence starting at 1
|
|
563
|
+
and the timestamp; producers cannot fake ordering.
|
|
564
|
+
- A batch whose serialized JSON exceeds 1,048,576 bytes is `413 validation`
|
|
565
|
+
naming the ceiling — the same bound and shape as the artifact ceiling.
|
|
566
|
+
- Appending to a closed stream is `409 conflict` (`stream "…" is closed`);
|
|
567
|
+
appending to an unknown stream is `404 not-found`. Neither writes anything.
|
|
568
|
+
- The server resolves and authorizes the stream **before** parsing the append
|
|
569
|
+
body, so a malformed or oversized batch on an unknown or foreign-scoped
|
|
570
|
+
stream is `404 not-found`, not `400 validation` — and the `413` ceiling is
|
|
571
|
+
likewise never reached, since the body is never evaluated. The `400` and
|
|
572
|
+
`413` rejections apply only after the stream resolves.
|
|
573
|
+
|
|
574
|
+
Read wait semantics: when no newer chunk exists and the stream is open, the
|
|
575
|
+
server may hold the request up to the parsed `wait` bound in whole seconds,
|
|
576
|
+
returning as soon as a chunk is appended or the stream closes, and no later
|
|
577
|
+
than the bound. A `wait` above 30 seconds is clamped to 30. Reads of a closed
|
|
578
|
+
stream never wait. A server may return before the bound at any time. Unlike
|
|
579
|
+
`since`, the `wait` value follows the strict digits grammar of section 3's
|
|
580
|
+
event-wait rules (a value that is not one or more ASCII digits is `400`
|
|
581
|
+
`validation` per section 9), with the 30-second stream ceiling as its clamp.
|
|
582
|
+
|
|
583
|
+
Close semantics: one atomic operation that assembles the chunks into the
|
|
584
|
+
protocol's `UIMessage[]` document — following the protocol for its defined
|
|
585
|
+
part types and dropping parts of undefined types while counting them —
|
|
586
|
+
deposits that document as an artifact on the owning scope with kind
|
|
587
|
+
`stream:<streamId>` at revision 0 and metadata naming the stream id, label,
|
|
588
|
+
scope, outcome, chunk count, and dropped-part count, and marks the stream
|
|
589
|
+
closed. If the artifact deposit fails, the stream stays open and nothing is
|
|
590
|
+
written. Closing an already-closed stream is a no-op that returns the record
|
|
591
|
+
(regardless of the requested outcome).
|
|
592
|
+
|
|
593
|
+
Chunk retention is deposit-path and count-based, like artifact retention: a
|
|
594
|
+
closed stream's chunks remain readable until the next stream is created in
|
|
595
|
+
the same scope, at which point the chunks of every previously closed stream
|
|
596
|
+
in that scope except the most recently closed one are deleted in the same
|
|
597
|
+
transaction as the create. Finalized `stream:*` artifacts are never touched
|
|
598
|
+
by this rule (their retention belongs to the open archival thread), and open
|
|
599
|
+
streams are never pruned. A read of a stream whose chunks were pruned returns
|
|
600
|
+
no chunks, `status: "closed"`, and the artifact reference.
|
|
601
|
+
|
|
602
|
+
These additions are purely additive: the protocol version stays `2`, and a
|
|
603
|
+
conforming server implements the stream routes together with this document's
|
|
604
|
+
rules exactly as it does for the event and artifact routes.
|
|
605
|
+
|
|
606
|
+
## 7. Authentication and token scope
|
|
607
|
+
|
|
608
|
+
### Open and authenticated modes
|
|
609
|
+
|
|
610
|
+
A server configured without a signing secret is in open mode: no route checks
|
|
611
|
+
a token or event session attribution. This is intended for local development
|
|
612
|
+
and the open contract harness.
|
|
613
|
+
|
|
614
|
+
A server configured with a secret requires this header on every `/builds`,
|
|
615
|
+
`/repos`, and `/sessions` route:
|
|
616
|
+
|
|
617
|
+
```http
|
|
618
|
+
Authorization: Bearer <token>
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
`GET /health` is always unauthenticated. Package/protocol identity validation
|
|
622
|
+
occurs first on machine routes. Authorization then occurs before request body
|
|
623
|
+
processing on collection routes and before resource lookup on resource routes.
|
|
624
|
+
A caller with the wrong scope therefore cannot use existence or validation
|
|
625
|
+
differences to inspect another resource.
|
|
626
|
+
|
|
627
|
+
### Token encoding
|
|
628
|
+
|
|
629
|
+
A token has two dot-separated base64url segments:
|
|
630
|
+
|
|
631
|
+
```text
|
|
632
|
+
base64url(UTF-8 JSON scope) + "." + base64url(HMAC-SHA256(secret, payloadSegment))
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
The HMAC input is the first token segment exactly as encoded, not the decoded
|
|
636
|
+
JSON bytes. The signature is compared in constant time. The scope JSON must
|
|
637
|
+
match exactly one of these shapes; unknown keys are invalid:
|
|
638
|
+
|
|
639
|
+
```jsonc
|
|
640
|
+
// Legacy build scope, retained for wire compatibility
|
|
641
|
+
{ "build": "build-slug", "session": "s_123", "exp": 1784116800000 }
|
|
642
|
+
|
|
643
|
+
// Explicit resource scope
|
|
644
|
+
{
|
|
645
|
+
"resource": { "kind": "build", "id": "build-slug" },
|
|
646
|
+
"session": "s_123",
|
|
647
|
+
"exp": 1784116800000
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
{
|
|
651
|
+
"resource": { "kind": "repo", "id": "acme/autobuild" },
|
|
652
|
+
"session": "hs_123",
|
|
653
|
+
"exp": 1784116800000
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
{
|
|
657
|
+
"resource": { "kind": "session", "id": "os_123" },
|
|
658
|
+
"session": "*",
|
|
659
|
+
"exp": 1784116800000
|
|
660
|
+
}
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
`build`, resource `id`, and `session` are nonempty strings. A resource or
|
|
664
|
+
operator scope may carry an optional `via` claim (the same shape as the
|
|
665
|
+
actor's `via` marker). `exp` is an integer
|
|
666
|
+
Unix epoch in **milliseconds**. A token is expired when `exp <=` the server's
|
|
667
|
+
current epoch milliseconds. A malformed token, malformed scope, bad signature,
|
|
668
|
+
or expired token is invalid and receives `401 auth` without a more specific
|
|
669
|
+
verification diagnostic.
|
|
670
|
+
|
|
671
|
+
The legacy scope `{"build":"*", ...}` is the admin resource scope. The
|
|
672
|
+
normal runner/admin form also uses `"session":"*"`, allowing event writes on
|
|
673
|
+
behalf of any valid actor. Only the legacy `build: "*"` spelling grants admin
|
|
674
|
+
resource access; an explicit resource whose id is `"*"` is not admin.
|
|
675
|
+
|
|
676
|
+
### Resource authorization matrix
|
|
677
|
+
|
|
678
|
+
| Token resource | `/builds` create/list | one matching build | `/repos` ensure | one matching repo | `/repos/{repo}/sessions` create/list | one matching session |
|
|
679
|
+
|---|---:|---:|---:|---:|---:|---:|
|
|
680
|
+
| admin (`build: "*"`) | yes | any | yes | any | yes | any |
|
|
681
|
+
| build | no | exact id only | no | no | no | no |
|
|
682
|
+
| repo | no | no | no | exact id only | yes | no |
|
|
683
|
+
| session | no | no | no | no | no | exact id only |
|
|
684
|
+
|
|
685
|
+
A valid token used for the wrong resource receives `403 auth`. Resource scope
|
|
686
|
+
gates all operations, including reads, artifact operations, and leases. A
|
|
687
|
+
session resource token's authority is exactly its one session — it cannot
|
|
688
|
+
create or list sessions, and it cannot touch another session's events,
|
|
689
|
+
artifacts, or streams. A repo token owns its repository's session collection
|
|
690
|
+
routes but not any session resource. Session resource and repo/build tokens
|
|
691
|
+
never cross.
|
|
692
|
+
|
|
693
|
+
The token's session dimension adds a second gate only to event-bearing writes:
|
|
694
|
+
`POST .../events`, `POST .../events/conditional`, and `POST .../deposits`.
|
|
695
|
+
|
|
696
|
+
- `session: "*"` may submit any actor that the event catalog itself allows.
|
|
697
|
+
- Any other session may submit only an actor with `kind: "agent"` and a
|
|
698
|
+
`session` exactly equal to the token session.
|
|
699
|
+
- Another agent session or a kernel, human, dispatcher, or ingester actor
|
|
700
|
+
receives `403 auth` before catalog validation.
|
|
701
|
+
- Reads, standalone artifact puts, and lease operations do not apply session
|
|
702
|
+
attribution beyond the resource-scope check.
|
|
703
|
+
|
|
704
|
+
### Delegated-write attribution (`via`)
|
|
705
|
+
|
|
706
|
+
On every event-bearing write (`POST .../events`, `POST .../events/conditional`,
|
|
707
|
+
and `POST .../deposits` on build, repository, and session resources), a server
|
|
708
|
+
in authenticated mode enforces `via` attribution after token verification and
|
|
709
|
+
before catalog validation:
|
|
710
|
+
|
|
711
|
+
- A human actor claiming a `via` the token does not carry is `403 auth` — the
|
|
712
|
+
caller must be able to distinguish "not your delegate" from "malformed
|
|
713
|
+
event", which is why this is authority (403) and not validation (422).
|
|
714
|
+
- A token carrying a `via` claim stamps that `via` onto every human actor in
|
|
715
|
+
the write: the token is authoritative, and a matching claim or no claim both
|
|
716
|
+
end with the token's `via` on the stored event.
|
|
717
|
+
- A non-human actor carrying a `via` key is not an authority question; it falls
|
|
718
|
+
through to catalog validation, which rejects it with the rule message.
|
|
719
|
+
- An unscoped operator token carries no `via` and never authorizes raw store
|
|
720
|
+
routes at all.
|
|
721
|
+
|
|
722
|
+
In open mode there is no token to be authoritative and writes pass through
|
|
723
|
+
unchanged. Events without a `via` member are stored, replayed, and read
|
|
724
|
+
byte-identically to before.
|
|
725
|
+
|
|
726
|
+
Build and repository scopes never cross: a harvest repository token cannot
|
|
727
|
+
read a build stream, and a build token cannot read a repository journal.
|
|
728
|
+
Attributed human-operator scopes have
|
|
729
|
+
`{ "operator": { "user": "nonblank identity" }, "exp": … }`. They are accepted
|
|
730
|
+
only by the [operator API](operator-api.md), never by raw store or ticket routes;
|
|
731
|
+
conversely, deployment/admin/build/repository scopes are refused there.
|
|
732
|
+
Deployment scopes have `{ "operator": true, "session": "*", "exp": … }` and
|
|
733
|
+
cover raw store and [hosted ticket](remote-ticket-protocol.md) routes without
|
|
734
|
+
granting attributed operator controls.
|
|
735
|
+
|
|
736
|
+
## 8. Atomic deposits
|
|
737
|
+
|
|
738
|
+
The in-process `appendWithArtifacts` APIs take a callback that receives
|
|
739
|
+
server-assigned artifact revisions. A callback cannot cross HTTP, so the wire
|
|
740
|
+
uses negative revision placeholders.
|
|
741
|
+
|
|
742
|
+
For request artifact at zero-based index `i`, the placeholder revision is:
|
|
743
|
+
|
|
744
|
+
```text
|
|
745
|
+
-(i + 1)
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
Thus the first artifact uses `-1`, the second `-2`, and so on. The client runs
|
|
749
|
+
its callback against sentinel metadata and sends the resulting event write.
|
|
750
|
+
The server must perform this algorithm:
|
|
751
|
+
|
|
752
|
+
1. Decode and prepare all request artifacts without publishing any artifact
|
|
753
|
+
metadata or event.
|
|
754
|
+
2. Inside the backing store's atomic `appendWithArtifacts` operation, assign
|
|
755
|
+
each artifact's real per-kind revision.
|
|
756
|
+
3. Recursively walk **the event payload**. Arrays and object values are walked.
|
|
757
|
+
4. Replace a value only when it is an object with exactly two own keys,
|
|
758
|
+
`kind` and `rev`, where `kind` is a string and `rev` is a negative integer.
|
|
759
|
+
5. Convert `rev` to index `-rev - 1`. If that request artifact exists, replace
|
|
760
|
+
the whole object with the assigned metadata's
|
|
761
|
+
`{"kind": meta.kind, "rev": meta.revision}`. The placeholder's supplied
|
|
762
|
+
`kind` does not override the deposited artifact's kind.
|
|
763
|
+
6. Leave an out-of-range placeholder unchanged. Catalog validation will reject
|
|
764
|
+
a negative revision wherever that value is governed by an artifact-ref
|
|
765
|
+
schema.
|
|
766
|
+
7. Validate the substituted event write, then make all artifact metadata and
|
|
767
|
+
the event visible as one commit.
|
|
768
|
+
|
|
769
|
+
Substitution does not inspect actor, type, artifact metadata, or arbitrary
|
|
770
|
+
objects with extra keys. It supports exact `{kind, rev}` references only;
|
|
771
|
+
arithmetic or string computation over a future revision is not part of the
|
|
772
|
+
protocol.
|
|
773
|
+
|
|
774
|
+
The all-or-nothing guarantee covers artifact metadata and the event: no
|
|
775
|
+
failure may expose one without the other, and a failed bundle consumes no
|
|
776
|
+
artifact revision or event sequence. A backing implementation may leave an
|
|
777
|
+
unreferenced content-addressed blob after failure, because blobs are immutable
|
|
778
|
+
and metadata is the visibility boundary. Revision assignment, substitution,
|
|
779
|
+
event validation, and metadata/event commit must be serialized or
|
|
780
|
+
transactional so concurrent deposits receive distinct revisions and events.
|
|
781
|
+
|
|
782
|
+
These rules apply identically to build, repository, and session deposits.
|
|
783
|
+
|
|
784
|
+
## 9. Errors and validation
|
|
785
|
+
|
|
786
|
+
Every non-success response is exactly the JSON error shape from section 2.
|
|
787
|
+
The shipped server maps failures as follows:
|
|
788
|
+
|
|
789
|
+
| Status | `kind` | Meaning |
|
|
790
|
+
|---:|---|---|
|
|
791
|
+
| `400` | `validation` | Invalid JSON, request body schema, malformed percent-encoding, missing/empty required `kind`, a `since`/`rev` value whose JavaScript `Number` conversion is not an integer, or a `wait` value that is not one or more ASCII digits |
|
|
792
|
+
| `401` | `auth` | Missing bearer credentials or an invalid, malformed, badly signed, or expired token |
|
|
793
|
+
| `403` | `auth` | Valid token with the wrong resource scope or event-session attribution |
|
|
794
|
+
| `404` | `not-found` | Unknown route, unsupported method, unknown build, unknown repository, unknown session, or an unknown or foreign-scoped stream |
|
|
795
|
+
| `409` | `conflict` | Missing/mismatched package or protocol identity, duplicate build creation, appending to a closed stream (`stream "…" is closed`), or a backing conflict reported as already existing |
|
|
796
|
+
| `413` | `validation` | Decoded artifact or serialized stream batch exceeds the configured ceiling; the message names the ceiling |
|
|
797
|
+
| `422` | `validation` | `EventValidationError` from build, repository, or session catalog validation; its message is preserved verbatim |
|
|
798
|
+
| `500` | `internal` | Any other unexpected backing or server failure; the thrown error message is returned |
|
|
799
|
+
|
|
800
|
+
Authentication runs before resource lookup, and session authorization runs
|
|
801
|
+
before event catalog validation. A request may therefore receive `401` or
|
|
802
|
+
`403` even if its resource or event body is also invalid.
|
|
803
|
+
|
|
804
|
+
Stream appends are evaluated in a fixed order — authentication, stream
|
|
805
|
+
resolution and scope authorization, body schema, backing-store validation and
|
|
806
|
+
batch ceiling, closed check — so each earlier rejection masks the later ones:
|
|
807
|
+
a request with both an unknown stream and an invalid body is `404`, and an
|
|
808
|
+
oversized batch addressed to an unknown stream is `404` too (the body, and
|
|
809
|
+
with it the ceiling, is never evaluated). Note this ordering differs from the
|
|
810
|
+
local store adapters, which validate the batch before resolving the stream —
|
|
811
|
+
the streams section of the store specification states both orders
|
|
812
|
+
authoritatively.
|
|
813
|
+
|
|
814
|
+
A `400` wire-schema rejection performs no backing mutation. A `422` event
|
|
815
|
+
rejection appends no event. For a deposit, either class of validation failure
|
|
816
|
+
also exposes no artifact metadata and consumes no revisions or sequences.
|
|
817
|
+
Unknown-resource writes are rejected and perform no mutation.
|
|
818
|
+
|
|
819
|
+
The generic wire event schema deliberately does not duplicate the event
|
|
820
|
+
catalog. Servers must call the matched validator — `validateEventWrite`,
|
|
821
|
+
`validateRepositoryEventWrite`, or `validateSessionEventWrite` — before every
|
|
822
|
+
ordinary append and inside every atomic deposit.
|
|
823
|
+
|
|
824
|
+
## 10. Lease and persistence requirements
|
|
825
|
+
|
|
826
|
+
A conforming backing server provides these behaviors for both build and
|
|
827
|
+
repository leases:
|
|
828
|
+
|
|
829
|
+
- A claim succeeds when the lease is absent, expired, or already belongs to
|
|
830
|
+
the same holder. Success stores the holder, the claim's TTL, and expiry
|
|
831
|
+
`now + ttlMs` and returns `{"ok":true}`.
|
|
832
|
+
- A different holder is rejected with `{"ok":false}` while the current lease
|
|
833
|
+
is unexpired. Expiry is strict: at `expiresAt <= now`, the lease is expired
|
|
834
|
+
and claimable.
|
|
835
|
+
- A same-holder claim renews the lease using the new claim TTL.
|
|
836
|
+
- A heartbeat succeeds only for the current holder while the lease is
|
|
837
|
+
unexpired. It records `heartbeatAt` and extends expiry to
|
|
838
|
+
`now +` the TTL from the successful claim. It does not accept a new TTL.
|
|
839
|
+
- A missing, expired, or wrong-holder heartbeat is a no-op returning
|
|
840
|
+
`{"ok":false}`.
|
|
841
|
+
- Release removes the lease only for its current holder. A wrong-holder
|
|
842
|
+
release is a no-op, but the HTTP response remains `{"ok":true}`.
|
|
843
|
+
- Claims, heartbeat checks, and release decisions are atomic under concurrent
|
|
844
|
+
callers.
|
|
845
|
+
|
|
846
|
+
Beyond leases, the backing store must maintain:
|
|
847
|
+
|
|
848
|
+
- per-build and per-repository monotonically assigned event sequences starting
|
|
849
|
+
at 1, preserving append order and continuity across server restarts;
|
|
850
|
+
- atomic build-stream conditional append, including a mutation-free stale
|
|
851
|
+
comparison result;
|
|
852
|
+
- server-assigned event, record, artifact, lease, and heartbeat times;
|
|
853
|
+
- 0-based revisions independently per resource and artifact kind, including
|
|
854
|
+
distinct concurrent assignments;
|
|
855
|
+
- SHA-256 content addresses and exact byte, metadata, and metadata-list round
|
|
856
|
+
trips;
|
|
857
|
+
- artifact lists ordered by kind and revision;
|
|
858
|
+
- strict event payload and actor validation before mutation;
|
|
859
|
+
- atomic deposit visibility and rollback as described in section 8; and
|
|
860
|
+
- rejection of event, artifact, and lease writes to unknown resources.
|
|
861
|
+
|
|
862
|
+
For streams, the backing store must additionally maintain:
|
|
863
|
+
|
|
864
|
+
- per-stream monotonically assigned chunk sequences starting at 1, preserving
|
|
865
|
+
append order and continuity across server restarts;
|
|
866
|
+
- append rejection (with no mutation) for closed and unknown streams and for
|
|
867
|
+
batches above the serialized-byte ceiling;
|
|
868
|
+
- the bounded read wait exactly as specified in section 6, including the
|
|
869
|
+
30-second clamp and no wait on closed streams;
|
|
870
|
+
- the bounded wait on the build, repository, and session event reads exactly
|
|
871
|
+
as specified in section 3, with no per-request database poll faster than once
|
|
872
|
+
per second (the shipped PostgreSQL adapter polls held reads at
|
|
873
|
+
`EVENT_WAIT_POLL_MS = 1000`, so an append by another connection is observed
|
|
874
|
+
at the next poll — typically within about one second, worst case one poll
|
|
875
|
+
interval plus the query round-trip; a hard ≤1 s worst case would need
|
|
876
|
+
LISTEN/NOTIFY wake-on-append at a dedicated connection per held request on
|
|
877
|
+
a pooled provider, a cost considered and declined);
|
|
878
|
+
- the atomic close — document assembly, artifact deposit, and the closed
|
|
879
|
+
record visible together or not at all, with the stream left open and
|
|
880
|
+
unwritten when the deposit fails;
|
|
881
|
+
- the chunk-retention prune at the next create in a scope, in the same
|
|
882
|
+
transaction, keeping the most recently closed stream's chunks; and
|
|
883
|
+
- stream ids that are globally unique and `st_`-prefixed.
|
|
884
|
+
|
|
885
|
+
## 11. Client-only behavior and health
|
|
886
|
+
|
|
887
|
+
Four shipped behaviors do not add `BuildStore` routes:
|
|
888
|
+
|
|
889
|
+
- `RemoteBuildStore.scopeBuild(slug)` and `RemoteBuildStore.scopeSession(id)`
|
|
890
|
+
return interface-enforced client handles for exactly that build or session.
|
|
891
|
+
Own-resource record, event, artifact, and (for builds) lease calls use the
|
|
892
|
+
existing routes below. Foreign-resource calls, collection/admin operations,
|
|
893
|
+
nested foreign scope, `close`, and every other family's operations fail in
|
|
894
|
+
the client before a request. This is required even with an open server or an
|
|
895
|
+
admin token: HTTP token scope carries authority over the wire as defense in
|
|
896
|
+
depth; it does not create Store scope.
|
|
897
|
+
- `RemoteBuildStore.subscribe(slug, options, onEvent)` polls
|
|
898
|
+
`GET /builds/{slug}/events?since=<lastSeq>`. It starts with
|
|
899
|
+
`options.fromSeq ?? 0`, delivers increasing sequence numbers exactly once
|
|
900
|
+
within that subscription, does not overlap polls, and ignores a polling
|
|
901
|
+
error, retrying on the next cycle. Calling the returned unsubscribe
|
|
902
|
+
function stops future delivery and aborts an in-flight held request. There
|
|
903
|
+
is no repository subscribe method.
|
|
904
|
+
Its cadence follows `options.waitSeconds` (AUT-334), which defaults to the
|
|
905
|
+
client's bounded window (25 s, `REMOTE_EVENT_WAIT_SECONDS`) when the caller
|
|
906
|
+
omits it — so a plain `subscribe` holds one request per wait window on a
|
|
907
|
+
quiet stream, and `waitSeconds: 0` restores the immediate interval loop
|
|
908
|
+
(one read, then one read per `options.pollMs ?? 250`). With a positive
|
|
909
|
+
`waitSeconds`, each cycle issues one held request (`wait=<waitSeconds>`, so
|
|
910
|
+
the server honors the section 3 bound) and then gap-fills so request
|
|
911
|
+
*starts* stay at least `options.pollMs ?? 250` apart, elapsed request time
|
|
912
|
+
counting toward the gap — degrading to exactly the interval rate against a
|
|
913
|
+
server that answers immediately. Delivery order and exactly-once semantics
|
|
914
|
+
are identical in all modes.
|
|
915
|
+
- `RemoteBuildStore.close()` is a no-op. The remote server owns its backing
|
|
916
|
+
store lifecycle; there is no close endpoint.
|
|
917
|
+
- `GET /health` is outside the `BuildStore` interface. It is always open and
|
|
918
|
+
returns exactly `200 {"ok":true,"autobuildVersion":"<package>",
|
|
919
|
+
"protocolVersion":"2"}`. It reports HTTP-process availability and identity,
|
|
920
|
+
not a deeper backing-store transaction or migration check.
|
|
921
|
+
|
|
922
|
+
There is no push/WebSocket subscription protocol, batch-read route,
|
|
923
|
+
repository listing, artifact deletion, or server deployment API.
|
|
924
|
+
|
|
925
|
+
## 12. Conformance
|
|
926
|
+
|
|
927
|
+
The compatibility bar is an HTTP-backed `BuildStore` client driving the
|
|
928
|
+
complete `describeBuildStoreContract` suite against a clean server. Both the
|
|
929
|
+
contract function and the `BuildStore` type are public exports from
|
|
930
|
+
`@defrex/autobuild/plugin-sdk`. A test registration has this shape:
|
|
931
|
+
|
|
932
|
+
```ts
|
|
933
|
+
import {
|
|
934
|
+
describeBuildStoreContract,
|
|
935
|
+
type BuildStore,
|
|
936
|
+
} from '@defrex/autobuild/plugin-sdk'
|
|
937
|
+
|
|
938
|
+
describeBuildStoreContract('my remote store', async (opts) => {
|
|
939
|
+
// Start a fresh, isolated server and database for every factory call.
|
|
940
|
+
// Pass opts?.clock to the server/backing store, or connect equivalent
|
|
941
|
+
// deterministic clock control in the external test harness.
|
|
942
|
+
const server = await startMyStore({ clock: opts?.clock })
|
|
943
|
+
const store: BuildStore = createHttpBackedBuildStore({
|
|
944
|
+
url: server.url,
|
|
945
|
+
// Omit against an open test server, or supply an admin token.
|
|
946
|
+
token: server.adminToken,
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
return {
|
|
950
|
+
store,
|
|
951
|
+
cleanup: async () => server.stopAndDeleteState(),
|
|
952
|
+
}
|
|
953
|
+
})
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
The factory must isolate state between tests. It must also connect the suite's
|
|
957
|
+
injected clock—or an equivalent controllable server clock—because the contract
|
|
958
|
+
asserts exact store timestamps, lease expiry, renewal, and heartbeat behavior.
|
|
959
|
+
If authentication is enabled, the harness must mint a valid admin token for
|
|
960
|
+
the controlled server time.
|
|
961
|
+
|
|
962
|
+
Add protocol-specific tests alongside the shared contract for token encoding
|
|
963
|
+
and resource isolation, session-attributed writes, exact validation feedback,
|
|
964
|
+
atomic placeholder substitution, event paging, unauthenticated health, and
|
|
965
|
+
continuity across server restarts. The session family (section 5), the
|
|
966
|
+
session-scoped token's authority matrix, and the `via` stamp/reject order
|
|
967
|
+
(section 7) are part of the contract suite and of the protocol-specific cases. Exercise all routes through HTTP, not by
|
|
968
|
+
calling the backing store directly.
|
|
969
|
+
|
|
970
|
+
Passing only route smoke tests is not conformance. The complete shared contract
|
|
971
|
+
driven through an HTTP-backed client, plus the protocol-specific cases above,
|
|
972
|
+
is the required compatibility test.
|