@defrex/autobuild 0.10.1 → 0.12.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/README.md +3 -1
- package/SPEC.md +237 -23
- package/bin/ab-dispatch-kernel.ts +1 -1
- package/docs/architecture.md +66 -4
- package/docs/configuration.md +137 -19
- package/docs/hosted-dispatcher.md +17 -2
- package/docs/hosted-service.md +214 -0
- package/docs/mcp.md +8 -2
- package/docs/operator-api.md +107 -7
- package/docs/remote-store-protocol.md +189 -144
- package/docs/remote-ticket-protocol.md +111 -18
- package/docs/setup.md +173 -16
- package/docs/ticket-dependencies.md +1 -1
- package/package.json +7 -6
- package/packages/core/src/cli/attention.ts +209 -0
- package/packages/core/src/cli/binary.ts +3 -3
- package/packages/core/src/cli/configured-store.ts +17 -0
- package/packages/core/src/cli/connect.ts +261 -0
- package/packages/core/src/cli/dashboard/model.ts +8 -5
- package/packages/core/src/cli/dashboard/render.ts +37 -7
- package/packages/core/src/cli/dispatch-frontend.ts +16 -1
- package/packages/core/src/cli/dispatch-process.ts +25 -7
- package/packages/core/src/cli/dispatch.ts +247 -47
- package/packages/core/src/cli/dispatcher-liveness.ts +91 -0
- package/packages/core/src/cli/help.ts +45 -13
- package/packages/core/src/cli/init-validation.ts +27 -2
- package/packages/core/src/cli/main.ts +41 -15
- package/packages/core/src/cli/mcp.ts +7 -1
- package/packages/core/src/cli/pr-summary-publication.ts +220 -0
- package/packages/core/src/cli/pr-summary.ts +2 -0
- package/packages/core/src/cli/project-credentials.ts +132 -0
- package/packages/core/src/cli/repo-state.ts +15 -2
- package/packages/core/src/cli/repository-status.ts +45 -5
- package/packages/core/src/cli/self-update.ts +10 -0
- package/packages/core/src/cli/store-opening.ts +22 -14
- package/packages/core/src/cli/terminals.ts +0 -10
- package/packages/core/src/cli/ticket.ts +8 -1
- package/packages/core/src/cli/upgrade-compatibility.ts +80 -0
- package/packages/core/src/cli/wait.ts +84 -17
- package/packages/core/src/cli/watch.ts +350 -68
- package/packages/core/src/config/live.ts +4 -0
- package/packages/core/src/config/schema.ts +65 -11
- package/packages/core/src/connection.ts +86 -0
- package/packages/core/src/events/catalog.ts +10 -2
- package/packages/core/src/events/globs.ts +8 -3
- package/packages/core/src/events/payloads.ts +44 -0
- package/packages/core/src/events/repository.ts +64 -2
- package/packages/core/src/integration/harness.ts +2 -1
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +148 -83
- package/packages/core/src/kernel/dispatch-status.ts +9 -1
- package/packages/core/src/kernel/dispatcher-liveness.ts +70 -0
- package/packages/core/src/kernel/engine.ts +24 -0
- package/packages/core/src/kernel/generators/build-log.ts +10 -0
- package/packages/core/src/kernel/log-index.ts +17 -0
- package/packages/core/src/kernel/pr-summary.ts +89 -0
- package/packages/core/src/kernel/reducer-registry.ts +12 -0
- package/packages/core/src/kernel/reducer.ts +8 -0
- package/packages/core/src/kernel/ticket-source-health.ts +30 -0
- package/packages/core/src/ontology.ts +1 -0
- package/packages/core/src/operator/index.ts +7 -0
- package/packages/core/src/operator/query.ts +44 -19
- package/packages/core/src/operator/ticket-source-backend.ts +1 -0
- package/packages/core/src/operator/tickets.ts +6 -8
- package/packages/core/src/plugin-sdk/index.ts +1 -0
- package/packages/core/src/plugins/manifest.ts +5 -3
- package/packages/core/src/plugins/runtimes.ts +3 -1
- package/packages/core/src/ports/forge/fake.ts +13 -1
- package/packages/core/src/ports/forge/github-transport.ts +36 -1
- package/packages/core/src/ports/forge/github.ts +29 -5
- package/packages/core/src/ports/forge/local-git.ts +12 -4
- package/packages/core/src/ports/forge/summary-marker.ts +8 -0
- package/packages/core/src/ports/runner/session-env.ts +1 -0
- package/packages/core/src/ports/tickets/contract.ts +25 -0
- package/packages/core/src/ports/tickets/create.ts +11 -3
- package/packages/core/src/ports/tickets/fake.ts +10 -8
- package/packages/core/src/ports/tickets/file.ts +1 -0
- package/packages/core/src/ports/tickets/hosted-index.ts +0 -3
- package/packages/core/src/ports/tickets/linear.ts +241 -49
- package/packages/core/src/ports/tickets/remote-protocol.ts +3 -0
- package/packages/core/src/ports/tickets/remote.ts +1 -0
- package/packages/core/src/ports/types.ts +17 -0
- package/packages/core/src/ports/workspace/build-execution.ts +5 -1
- package/packages/core/src/ports/workspace/create.ts +11 -2
- package/packages/core/src/ports/workspace/distribution-archive.ts +3 -5
- package/packages/core/src/ports/workspace/harvest-execution.ts +3 -1
- package/packages/core/src/ports/workspace/local-build-execution.ts +8 -1
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +47 -19
- package/packages/core/src/processes/build-execution-state.ts +1 -1
- package/packages/core/src/processes/build-runner.ts +87 -26
- package/packages/core/src/processes/dispatch-failure-budget.ts +83 -0
- package/packages/core/src/processes/dispatcher.ts +336 -83
- package/packages/core/src/processes/harvest-child.ts +1 -0
- package/packages/core/src/processes/harvest-execution-state.ts +19 -2
- package/packages/core/src/processes/harvest-runner.ts +142 -100
- package/packages/core/src/processes/phase-failure-budget.ts +1 -0
- package/packages/core/src/processes/publication-loss.ts +18 -1
- package/packages/core/src/processes/repo-view.ts +2 -1
- package/packages/core/src/store/adapter.ts +1 -0
- package/packages/core/src/store/contract.ts +109 -0
- package/packages/core/src/store/digest.ts +14 -2
- package/packages/core/src/store/local/store.ts +18 -5
- package/packages/core/src/store/remote/client.ts +1 -1
- package/packages/core/src/store/remote/compatibility.ts +90 -0
- package/packages/core/src/store/remote/credentials.ts +28 -0
- package/packages/core/src/store/remote/index.ts +14 -1
- package/packages/core/src/store/remote/protocol.ts +26 -0
- package/packages/core/src/store/remote/token.ts +74 -13
- package/packages/core/src/store/repo-state-events.ts +49 -59
- package/packages/core/src/store/types.ts +5 -1
- package/packages/core/src/testing/index.ts +7 -0
- package/skills/guide/SKILL.md +175 -26
- package/skills/guide/references/operator-api.md +135 -13
- package/skills/guide/references/plugin-authoring.md +10 -2
- package/skills/guide/references/remote-store-protocol.md +189 -144
- package/skills/guide/references/remote-ticket-protocol.md +111 -18
- package/skills/guide/references/setup.md +173 -16
- package/skills/guide/references/ticket-dependencies.md +1 -1
- package/skills/operate/SKILL.md +159 -34
- package/templates/hosted-service/package.json +13 -0
- package/templates/hosted-service/vercel.json +7 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { delegateCredential } from '../../store/remote/credentials'
|
|
1
2
|
import { resolve } from 'node:path'
|
|
2
3
|
import { SANDBOX_FORBIDDEN_ENV } from './operator-sandbox'
|
|
3
4
|
import type { WorkspaceConfig } from '../../config/schema'
|
|
@@ -131,10 +132,18 @@ export async function createWorkspaceProvider(
|
|
|
131
132
|
// receives through `opts` reach the plugin factory through the context.
|
|
132
133
|
return await factory({
|
|
133
134
|
config: config.config,
|
|
134
|
-
env: opts.env,
|
|
135
|
+
env: Object.fromEntries(Object.entries(opts.env).filter(([key]) => key !== 'AB_TOKEN')),
|
|
135
136
|
repoRoot: resolve(opts.repoRoot),
|
|
136
137
|
...(opts.storeRef !== undefined ? { storeRef: opts.storeRef } : {}),
|
|
137
|
-
...(opts.
|
|
138
|
+
...(opts.storeRef !== undefined && opts.storeToken !== undefined
|
|
139
|
+
? {
|
|
140
|
+
delegateCredential: (
|
|
141
|
+
_ref: string,
|
|
142
|
+
_token: string | undefined,
|
|
143
|
+
target: Parameters<typeof delegateCredential>[2],
|
|
144
|
+
) => delegateCredential(opts.storeRef!, opts.storeToken, target),
|
|
145
|
+
}
|
|
146
|
+
: {}),
|
|
138
147
|
...(opts.runtimeReferences !== undefined
|
|
139
148
|
? { runtimeReferences: opts.runtimeReferences }
|
|
140
149
|
: {}),
|
|
@@ -18,11 +18,9 @@
|
|
|
18
18
|
* version in both modes: origin mode's release-asset tag is the running
|
|
19
19
|
* version by construction, and source mode packs `distributionRoot()`, whose
|
|
20
20
|
* package.json is the same file `readDistributionIdentity()` reads. The hosted
|
|
21
|
-
* remote store
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* move together through a release cut (`tools/release.ts`) plus
|
|
25
|
-
* store/dispatcher upgrades; there is no code path that safely mixes versions.
|
|
21
|
+
* remote store accepts only its published client release-line window and known
|
|
22
|
+
* protocol identities (409 outside that contract). Upgrade the service before
|
|
23
|
+
* a dispatcher moves beyond its window; projects can then upgrade independently.
|
|
26
24
|
* Provisioning records the installed distribution's identity stamp
|
|
27
25
|
* (`readDistributionIdentityStamp()`) in the guest's `.distribution-version`
|
|
28
26
|
* marker and reinstalls the archive on a later mismatch, so an upgraded
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
BuildExecutionHandle,
|
|
3
3
|
BuildExecutionIdentity,
|
|
4
4
|
ExecutionObservation,
|
|
5
|
+
ExecutionLaunchAuth,
|
|
5
6
|
} from './build-execution'
|
|
6
7
|
|
|
7
8
|
/** Guest environment variable carrying the serialized harvest launch
|
|
@@ -23,6 +24,7 @@ export interface HarvestExecutionEnvironment {
|
|
|
23
24
|
* advisory. Run state, config, and outcomes are obtained only through the
|
|
24
25
|
* repository journal in the Store. */
|
|
25
26
|
export interface HarvestExecutionStart {
|
|
27
|
+
preparedRun?: { run: string }
|
|
26
28
|
storeRef: string
|
|
27
29
|
/** Repository identity (normalized origin) the guest serves. */
|
|
28
30
|
repo: string
|
|
@@ -101,7 +103,7 @@ export function parseHarvestRunnerLaunch(value: unknown): HarvestRunnerLaunch |
|
|
|
101
103
|
* observation types so supervision semantics (long-poll wait, teardown
|
|
102
104
|
* outcome, provider liveness) are identical to builds. */
|
|
103
105
|
export interface HarvestExecution {
|
|
104
|
-
start(input: HarvestExecutionStart): Promise<BuildExecutionHandle>
|
|
106
|
+
start(input: HarvestExecutionStart, auth?: ExecutionLaunchAuth): Promise<BuildExecutionHandle>
|
|
105
107
|
/** Optional liveness observation of a previously recorded execution, from
|
|
106
108
|
* provider state alone. Absent = this provider cannot observe; callers
|
|
107
109
|
* supervise exactly as before ("cannot observe" never means "act"). */
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
BuildExecutionExit,
|
|
9
9
|
BuildExecutionHandle,
|
|
10
10
|
BuildExecutionStart,
|
|
11
|
+
ExecutionLaunchAuth,
|
|
11
12
|
} from './build-execution'
|
|
12
13
|
|
|
13
14
|
export const BUILD_RUNNER_OPTIONS_ENV = 'AB_BUILD_RUNNER_OPTIONS'
|
|
@@ -34,10 +35,16 @@ export class LocalBuildExecution implements BuildExecution {
|
|
|
34
35
|
this.spawn = opts.spawn ?? Bun.spawn
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
async start(
|
|
38
|
+
async start(
|
|
39
|
+
input: BuildExecutionStart,
|
|
40
|
+
auth?: ExecutionLaunchAuth,
|
|
41
|
+
): Promise<BuildExecutionHandle> {
|
|
42
|
+
if (/^https?:/i.test(input.storeRef) && !auth)
|
|
43
|
+
throw new Error('remote build launch requires delegated authority')
|
|
38
44
|
const child = this.spawn([process.execPath, this.entrypoint], {
|
|
39
45
|
env: {
|
|
40
46
|
...this.env,
|
|
47
|
+
AB_TOKEN: auth?.token,
|
|
41
48
|
[BUILD_RUNNER_OPTIONS_ENV]: JSON.stringify({
|
|
42
49
|
...input,
|
|
43
50
|
supervision: { kind: 'local-parent', parentPid: process.pid },
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { delegateCredential } from '../../store/remote/credentials'
|
|
2
|
+
import type { ExecutionLaunchAuth } from './build-execution'
|
|
1
3
|
import { createHash } from 'node:crypto'
|
|
2
4
|
import { Sandbox, Snapshot, type NetworkPolicy, type SandboxRegion } from '@vercel/sandbox'
|
|
3
5
|
import { displayName, tomlKey, type RuntimeReferenceGroup } from '../../config/roles'
|
|
@@ -638,7 +640,9 @@ function runtimeEnvironment(
|
|
|
638
640
|
env: Record<string, string | undefined>,
|
|
639
641
|
): Record<string, string> {
|
|
640
642
|
return Object.fromEntries(
|
|
641
|
-
config.environmentVariables
|
|
643
|
+
config.environmentVariables
|
|
644
|
+
.filter((name) => name !== 'AB_TOKEN')
|
|
645
|
+
.map((name) => [name, requireVercelEnvironmentValue(env, name)]),
|
|
642
646
|
)
|
|
643
647
|
}
|
|
644
648
|
|
|
@@ -942,10 +946,9 @@ export async function validateVercelSandbox(
|
|
|
942
946
|
env: Object.fromEntries([
|
|
943
947
|
['AB_STORE', options.storeRef],
|
|
944
948
|
['AB_TOKEN', options.storeToken],
|
|
945
|
-
...config.environmentVariables
|
|
946
|
-
name
|
|
947
|
-
requireVercelEnvironmentValue(options.env, name),
|
|
948
|
-
]),
|
|
949
|
+
...config.environmentVariables
|
|
950
|
+
.filter((name) => name !== 'AB_TOKEN')
|
|
951
|
+
.map((name) => [name, requireVercelEnvironmentValue(options.env, name)]),
|
|
949
952
|
]),
|
|
950
953
|
}),
|
|
951
954
|
)
|
|
@@ -1106,10 +1109,11 @@ async function effectiveArchive(
|
|
|
1106
1109
|
}
|
|
1107
1110
|
|
|
1108
1111
|
export interface VercelSandboxProviderOptions {
|
|
1112
|
+
delegateCredential?: typeof delegateCredential
|
|
1109
1113
|
config: VercelSandboxConfig
|
|
1110
1114
|
env: Record<string, string | undefined>
|
|
1111
1115
|
storeRef: string
|
|
1112
|
-
storeToken
|
|
1116
|
+
storeToken?: string
|
|
1113
1117
|
/** Dispatcher-local main checkout, used only for origin discovery and verification.
|
|
1114
1118
|
* With the injected `origin`/`remoteBranchHead` seams (checkout-less origin
|
|
1115
1119
|
* mode) it is never touched on the host. */
|
|
@@ -1188,18 +1192,21 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1188
1192
|
}
|
|
1189
1193
|
|
|
1190
1194
|
constructor(private readonly options: VercelSandboxProviderOptions) {
|
|
1191
|
-
if (
|
|
1195
|
+
if (
|
|
1196
|
+
!/^https:\/\//i.test(options.storeRef) ||
|
|
1197
|
+
(!options.storeToken && !options.delegateCredential)
|
|
1198
|
+
) {
|
|
1192
1199
|
throw new Error('vercel-sandbox requires an HTTPS BuildStore and scoped AB_TOKEN authority')
|
|
1193
1200
|
}
|
|
1194
1201
|
this.facade = options.facade ?? createVercelSdkFacade(options.env)
|
|
1195
1202
|
this.exec = options.exec ?? spawnExec
|
|
1196
1203
|
this.observeWaitMs = options.observeWaitMs ?? VERCEL_OBSERVE_WAIT_MS
|
|
1197
1204
|
this.buildExecution = {
|
|
1198
|
-
start: (input) => this.start(input),
|
|
1205
|
+
start: (input, auth) => this.start(input, auth),
|
|
1199
1206
|
observe: (identity) => this.observeExecution(identity),
|
|
1200
1207
|
}
|
|
1201
1208
|
this.harvestExecution = {
|
|
1202
|
-
start: (input) => this.startHarvestExecution(input),
|
|
1209
|
+
start: (input, auth) => this.startHarvestExecution(input, auth),
|
|
1203
1210
|
observe: (identity) => this.observeExecution(identity),
|
|
1204
1211
|
}
|
|
1205
1212
|
this.orchestratorSandbox = {
|
|
@@ -1244,6 +1251,11 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1244
1251
|
revision?: string
|
|
1245
1252
|
generation?: number
|
|
1246
1253
|
}): Promise<WorkspaceProvisionResult> {
|
|
1254
|
+
const scopedToken = await (this.options.delegateCredential ?? delegateCredential)(
|
|
1255
|
+
this.options.storeRef,
|
|
1256
|
+
this.options.storeToken,
|
|
1257
|
+
{ kind: 'build-runner', id: opts.branch.replace(/^ab\//, '') },
|
|
1258
|
+
)
|
|
1247
1259
|
const rawOrigin = await this.origin()
|
|
1248
1260
|
const origin = cleanGithubOrigin(rawOrigin)
|
|
1249
1261
|
const name = sandboxName(origin.url, opts.branch, opts.generation)
|
|
@@ -1385,7 +1397,7 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1385
1397
|
await bootstrapRuntimes(
|
|
1386
1398
|
sandbox,
|
|
1387
1399
|
this.options.config,
|
|
1388
|
-
runtimeEnvironment(this.options.config, this.options.env),
|
|
1400
|
+
{ ...runtimeEnvironment(this.options.config, this.options.env), AB_TOKEN: scopedToken },
|
|
1389
1401
|
currentRuntimeReferences(this.options.runtimeReferences),
|
|
1390
1402
|
true,
|
|
1391
1403
|
)
|
|
@@ -1521,7 +1533,17 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1521
1533
|
}
|
|
1522
1534
|
}
|
|
1523
1535
|
|
|
1524
|
-
private async start(
|
|
1536
|
+
private async start(
|
|
1537
|
+
input: BuildExecutionStart,
|
|
1538
|
+
auth?: ExecutionLaunchAuth,
|
|
1539
|
+
): Promise<BuildExecutionHandle> {
|
|
1540
|
+
const scopedToken =
|
|
1541
|
+
auth?.token ??
|
|
1542
|
+
(await (this.options.delegateCredential ?? delegateCredential)(
|
|
1543
|
+
this.options.storeRef,
|
|
1544
|
+
this.options.storeToken,
|
|
1545
|
+
{ kind: 'build-runner', id: input.slug },
|
|
1546
|
+
))
|
|
1525
1547
|
const ref = input.workspaceRef
|
|
1526
1548
|
if (this.active.has(ref)) throw new Error(`sandbox ${ref} already has a live execution`)
|
|
1527
1549
|
const sandbox = this.sessions.get(ref) ?? (await this.facade.get(ref, this.operationSignal()))
|
|
@@ -1566,21 +1588,21 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1566
1588
|
await bootstrapRuntimes(
|
|
1567
1589
|
sandbox,
|
|
1568
1590
|
this.options.config,
|
|
1569
|
-
runtimeEnvironment(this.options.config, this.options.env),
|
|
1591
|
+
{ ...runtimeEnvironment(this.options.config, this.options.env), AB_TOKEN: scopedToken },
|
|
1570
1592
|
currentRuntimeReferences(this.options.runtimeReferences),
|
|
1571
1593
|
false,
|
|
1572
1594
|
)
|
|
1573
1595
|
this.sessions.set(ref, sandbox)
|
|
1574
1596
|
const env: Record<string, string> = {
|
|
1575
1597
|
AB_STORE: this.options.storeRef,
|
|
1576
|
-
AB_TOKEN:
|
|
1598
|
+
AB_TOKEN: scopedToken,
|
|
1577
1599
|
[BUILD_RUNNER_OPTIONS_ENV]: JSON.stringify({
|
|
1578
1600
|
...input,
|
|
1579
1601
|
supervision: { kind: 'environment' },
|
|
1580
1602
|
}),
|
|
1581
1603
|
}
|
|
1582
1604
|
for (const name of this.options.config.environmentVariables)
|
|
1583
|
-
env[name] = requireVercelEnvironmentValue(this.options.env, name)
|
|
1605
|
+
if (name !== 'AB_TOKEN') env[name] = requireVercelEnvironmentValue(this.options.env, name)
|
|
1584
1606
|
const command = (await sandbox.runCommand({
|
|
1585
1607
|
cmd: 'sh',
|
|
1586
1608
|
args: [
|
|
@@ -1696,7 +1718,12 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1696
1718
|
* build execution. Provisioning reuses the module-local build chain so a
|
|
1697
1719
|
* harvest guest is identical to a build guest; release is the provider's
|
|
1698
1720
|
* existing reap path (stop, snapshot purge, delete). */
|
|
1699
|
-
private async startHarvestExecution(
|
|
1721
|
+
private async startHarvestExecution(
|
|
1722
|
+
input: HarvestExecutionStart,
|
|
1723
|
+
auth?: ExecutionLaunchAuth,
|
|
1724
|
+
): Promise<BuildExecutionHandle> {
|
|
1725
|
+
if (!auth) throw new Error('harvest launch requires delegated authority')
|
|
1726
|
+
const scopedToken = auth.token
|
|
1700
1727
|
const origin = cleanGithubOrigin(await this.origin())
|
|
1701
1728
|
const name = harvestSandboxName(origin.url)
|
|
1702
1729
|
if (this.active.has(name)) throw new Error(`sandbox ${name} already has a live execution`)
|
|
@@ -1824,7 +1851,7 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1824
1851
|
await bootstrapRuntimes(
|
|
1825
1852
|
sandbox,
|
|
1826
1853
|
this.options.config,
|
|
1827
|
-
runtimeEnvironment(this.options.config, this.options.env),
|
|
1854
|
+
{ ...runtimeEnvironment(this.options.config, this.options.env), AB_TOKEN: scopedToken },
|
|
1828
1855
|
currentRuntimeReferences(this.options.runtimeReferences),
|
|
1829
1856
|
true,
|
|
1830
1857
|
)
|
|
@@ -1861,7 +1888,7 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1861
1888
|
await bootstrapRuntimes(
|
|
1862
1889
|
sandbox,
|
|
1863
1890
|
this.options.config,
|
|
1864
|
-
runtimeEnvironment(this.options.config, this.options.env),
|
|
1891
|
+
{ ...runtimeEnvironment(this.options.config, this.options.env), AB_TOKEN: scopedToken },
|
|
1865
1892
|
currentRuntimeReferences(this.options.runtimeReferences),
|
|
1866
1893
|
false,
|
|
1867
1894
|
)
|
|
@@ -1878,11 +1905,12 @@ export class VercelSandboxProvider implements WorkspaceProvider {
|
|
|
1878
1905
|
}
|
|
1879
1906
|
const env: Record<string, string> = {
|
|
1880
1907
|
AB_STORE: this.options.storeRef,
|
|
1881
|
-
AB_TOKEN:
|
|
1908
|
+
AB_TOKEN: scopedToken,
|
|
1882
1909
|
[HARVEST_RUNNER_OPTIONS_ENV]: JSON.stringify(envelope),
|
|
1883
1910
|
}
|
|
1884
1911
|
for (const envName of this.options.config.environmentVariables)
|
|
1885
|
-
|
|
1912
|
+
if (envName !== 'AB_TOKEN')
|
|
1913
|
+
env[envName] = requireVercelEnvironmentValue(this.options.env, envName)
|
|
1886
1914
|
const command = (await sandbox.runCommand({
|
|
1887
1915
|
cmd: 'sh',
|
|
1888
1916
|
args: [
|
|
@@ -22,7 +22,7 @@ export function effectiveBuildConfigContent(config: Config): string {
|
|
|
22
22
|
const { verify, finalize, ...root } = config
|
|
23
23
|
return JSON.stringify({
|
|
24
24
|
...root,
|
|
25
|
-
verify: { steps: verify.steps, ...verify.stepConfigs },
|
|
25
|
+
verify: { steps: verify.steps, afterReconcile: verify.afterReconcile, ...verify.stepConfigs },
|
|
26
26
|
finalize: { steps: finalize.steps, ...finalize.stepConfigs },
|
|
27
27
|
})
|
|
28
28
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { delegateCredential } from '../store/remote/credentials'
|
|
2
|
+
import { refreshSummaryBoundary } from '../cli/pr-summary-publication'
|
|
1
3
|
/**
|
|
2
4
|
* The build-runner process (SPEC §3.3): one per build, owns one pipeline
|
|
3
5
|
* execution end to end. The kernel's job reduced to a loop (§8): read the
|
|
@@ -170,6 +172,7 @@ export interface BuildRunnerDeps {
|
|
|
170
172
|
/** Extra AB_* the launcher wants in every session env (e.g. AB_TOKEN,
|
|
171
173
|
* AB_STORE for a remote store) — D8. May override AB_STORE; never the
|
|
172
174
|
* per-session identity keys (AB_BUILD/AB_PHASE/AB_SESSION). */
|
|
175
|
+
delegateCredential?: typeof delegateCredential
|
|
173
176
|
sessionEnv?: Record<string, string>
|
|
174
177
|
opts?: BuildRunnerOpts
|
|
175
178
|
}
|
|
@@ -819,6 +822,26 @@ export class BuildRunner {
|
|
|
819
822
|
const events = await store.getEvents(slug)
|
|
820
823
|
const decision = decideNext(events, config)
|
|
821
824
|
switch (decision.kind) {
|
|
825
|
+
case 'capture-reconcile-policy':
|
|
826
|
+
await store.append(slug, {
|
|
827
|
+
actor: KERNEL,
|
|
828
|
+
type: 'verify.reconcile-policy-captured',
|
|
829
|
+
payload: {
|
|
830
|
+
reconcileSeq: decision.reconcileSeq,
|
|
831
|
+
steps: config.verify.steps.map((step) => {
|
|
832
|
+
const override = config.verify.stepConfigs[step]?.afterReconcile
|
|
833
|
+
return {
|
|
834
|
+
step,
|
|
835
|
+
enabled: override ?? config.verify.afterReconcile,
|
|
836
|
+
source:
|
|
837
|
+
override === undefined
|
|
838
|
+
? 'verify.afterReconcile'
|
|
839
|
+
: `verify.${step}.afterReconcile`,
|
|
840
|
+
}
|
|
841
|
+
}),
|
|
842
|
+
},
|
|
843
|
+
})
|
|
844
|
+
return decision
|
|
822
845
|
case 'wait':
|
|
823
846
|
return decision
|
|
824
847
|
case 'acknowledge':
|
|
@@ -866,12 +889,15 @@ export class BuildRunner {
|
|
|
866
889
|
}
|
|
867
890
|
}
|
|
868
891
|
try {
|
|
892
|
+
if (await this.refreshSummary())
|
|
893
|
+
return reduceBuild(await this.deps.store.getEvents(this.deps.slug))
|
|
869
894
|
if (publicationPending(await this.deps.store.getEvents(this.deps.slug))) {
|
|
870
895
|
return reduceBuild(await this.deps.store.getEvents(this.deps.slug))
|
|
871
896
|
}
|
|
872
897
|
for (;;) {
|
|
873
898
|
await this.ensureLease()
|
|
874
899
|
const decision = await this.step()
|
|
900
|
+
if (await this.refreshSummary()) break
|
|
875
901
|
if (decision.kind === 'wait') break
|
|
876
902
|
if (publicationPending(await this.deps.store.getEvents(this.deps.slug))) break
|
|
877
903
|
}
|
|
@@ -882,6 +908,22 @@ export class BuildRunner {
|
|
|
882
908
|
}
|
|
883
909
|
}
|
|
884
910
|
|
|
911
|
+
private async refreshSummary(): Promise<boolean> {
|
|
912
|
+
try {
|
|
913
|
+
await this.ensureLease()
|
|
914
|
+
} catch {
|
|
915
|
+
return false
|
|
916
|
+
}
|
|
917
|
+
return refreshSummaryBoundary({
|
|
918
|
+
store: this.deps.store,
|
|
919
|
+
forge: this.deps.forge,
|
|
920
|
+
slug: this.deps.slug,
|
|
921
|
+
workspacePath: this.deps.workspacePath,
|
|
922
|
+
storeRef: this.deps.sessionEnv?.AB_STORE ?? 'local',
|
|
923
|
+
clock: this.deps.clock,
|
|
924
|
+
})
|
|
925
|
+
}
|
|
926
|
+
|
|
885
927
|
/**
|
|
886
928
|
* §7.4 exclusivity, holder side: a lapsed lease means the sweep may already
|
|
887
929
|
* have launched a replacement, so this runner must re-claim before doing
|
|
@@ -1146,6 +1188,7 @@ export class BuildRunner {
|
|
|
1146
1188
|
attempt: decision.attempt,
|
|
1147
1189
|
outcome: 'skipped',
|
|
1148
1190
|
reason: decision.reason,
|
|
1191
|
+
...(decision.skipCause !== undefined ? { skipCause: decision.skipCause } : {}),
|
|
1149
1192
|
},
|
|
1150
1193
|
} satisfies EventWrite<'verify.completed'>)
|
|
1151
1194
|
}
|
|
@@ -1254,6 +1297,11 @@ export class BuildRunner {
|
|
|
1254
1297
|
events: AbEvent[],
|
|
1255
1298
|
): Promise<void> {
|
|
1256
1299
|
const { step, action } = decision
|
|
1300
|
+
await this.deps.store.append(this.deps.slug, {
|
|
1301
|
+
actor: KERNEL,
|
|
1302
|
+
type: 'finalize.step-started',
|
|
1303
|
+
payload: { step },
|
|
1304
|
+
})
|
|
1257
1305
|
let actor: Actor = KERNEL
|
|
1258
1306
|
let durableHead: string | undefined
|
|
1259
1307
|
let failureNote: string | undefined
|
|
@@ -1383,7 +1431,7 @@ export class BuildRunner {
|
|
|
1383
1431
|
const turn = await this.runBudgetedTurn(
|
|
1384
1432
|
started.seq,
|
|
1385
1433
|
resolved.sessionBudgetSeconds,
|
|
1386
|
-
(signal) =>
|
|
1434
|
+
async (signal) =>
|
|
1387
1435
|
target.runner.start({
|
|
1388
1436
|
skill,
|
|
1389
1437
|
invocation: slug,
|
|
@@ -1391,7 +1439,7 @@ export class BuildRunner {
|
|
|
1391
1439
|
workspacePath,
|
|
1392
1440
|
...(target.model !== undefined ? { model: target.model } : {}),
|
|
1393
1441
|
...(target.args !== undefined ? { args: target.args } : {}),
|
|
1394
|
-
env: this.sessionEnvFor('finalize@1', session),
|
|
1442
|
+
env: await this.sessionEnvFor('finalize@1', session),
|
|
1395
1443
|
signal,
|
|
1396
1444
|
...(stream !== undefined ? { stream } : {}),
|
|
1397
1445
|
}),
|
|
@@ -1523,7 +1571,7 @@ export class BuildRunner {
|
|
|
1523
1571
|
|
|
1524
1572
|
let handle: AgentSessionHandle | undefined
|
|
1525
1573
|
let failureNote: string | undefined
|
|
1526
|
-
const turn = await this.runBudgetedTurn(started.seq, sessionBudgetSeconds, (signal) =>
|
|
1574
|
+
const turn = await this.runBudgetedTurn(started.seq, sessionBudgetSeconds, async (signal) =>
|
|
1527
1575
|
runner.start({
|
|
1528
1576
|
skill,
|
|
1529
1577
|
invocation: slug,
|
|
@@ -1531,7 +1579,7 @@ export class BuildRunner {
|
|
|
1531
1579
|
workspacePath,
|
|
1532
1580
|
...(model !== undefined ? { model } : {}),
|
|
1533
1581
|
...(args !== undefined ? { args } : {}),
|
|
1534
|
-
env: this.sessionEnvFor('finalize@1', session),
|
|
1582
|
+
env: await this.sessionEnvFor('finalize@1', session),
|
|
1535
1583
|
signal,
|
|
1536
1584
|
...(stream !== undefined ? { stream } : {}),
|
|
1537
1585
|
}),
|
|
@@ -1907,26 +1955,29 @@ export class BuildRunner {
|
|
|
1907
1955
|
let handle: AgentSessionHandle | undefined = live?.handle
|
|
1908
1956
|
let result: AgentTurnResult | undefined
|
|
1909
1957
|
let turnError: unknown
|
|
1910
|
-
const turn = await this.runBudgetedTurn(
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1958
|
+
const turn = await this.runBudgetedTurn(
|
|
1959
|
+
preSeq,
|
|
1960
|
+
primary.sessionBudgetSeconds,
|
|
1961
|
+
async (signal) =>
|
|
1962
|
+
live !== undefined
|
|
1963
|
+
? live.runner
|
|
1964
|
+
.continue(live.handle, continueMessage(spec), {
|
|
1965
|
+
env: await this.sessionEnvFor(spec.abPhase, session),
|
|
1966
|
+
signal,
|
|
1967
|
+
...(stream !== undefined ? { stream } : {}),
|
|
1968
|
+
})
|
|
1969
|
+
.then((continued) => ({ session: live.handle, result: continued }))
|
|
1970
|
+
: target.runner.start({
|
|
1971
|
+
skill: spec.skill,
|
|
1972
|
+
invocation: slug,
|
|
1973
|
+
buildSlug: slug,
|
|
1974
|
+
workspacePath,
|
|
1975
|
+
...(target.model !== undefined ? { model: target.model } : {}),
|
|
1976
|
+
...(target.args !== undefined ? { args: target.args } : {}),
|
|
1977
|
+
env: await this.sessionEnvFor(spec.abPhase, session),
|
|
1915
1978
|
signal,
|
|
1916
1979
|
...(stream !== undefined ? { stream } : {}),
|
|
1917
|
-
})
|
|
1918
|
-
.then((continued) => ({ session: live.handle, result: continued }))
|
|
1919
|
-
: target.runner.start({
|
|
1920
|
-
skill: spec.skill,
|
|
1921
|
-
invocation: slug,
|
|
1922
|
-
buildSlug: slug,
|
|
1923
|
-
workspacePath,
|
|
1924
|
-
...(target.model !== undefined ? { model: target.model } : {}),
|
|
1925
|
-
...(target.args !== undefined ? { args: target.args } : {}),
|
|
1926
|
-
env: this.sessionEnvFor(spec.abPhase, session),
|
|
1927
|
-
signal,
|
|
1928
|
-
...(stream !== undefined ? { stream } : {}),
|
|
1929
|
-
}),
|
|
1980
|
+
}),
|
|
1930
1981
|
)
|
|
1931
1982
|
if (turn.settlement.kind === 'value') {
|
|
1932
1983
|
handle = turn.settlement.value.session
|
|
@@ -2192,7 +2243,7 @@ export class BuildRunner {
|
|
|
2192
2243
|
let handle: AgentSessionHandle | undefined = live?.handle
|
|
2193
2244
|
let result: AgentTurnResult | undefined
|
|
2194
2245
|
let turnError: unknown
|
|
2195
|
-
const turn = await this.runBudgetedTurn(preSeq, sessionBudgetSeconds, (signal) => {
|
|
2246
|
+
const turn = await this.runBudgetedTurn(preSeq, sessionBudgetSeconds, async (signal) => {
|
|
2196
2247
|
if (live !== undefined) {
|
|
2197
2248
|
// §10: the producer continues its session; the feedback message
|
|
2198
2249
|
// points at the .ab/ inputs — `ab context` is the real carrier. The
|
|
@@ -2202,7 +2253,7 @@ export class BuildRunner {
|
|
|
2202
2253
|
// 1's second terminal (§8.4 D5).
|
|
2203
2254
|
return live.runner
|
|
2204
2255
|
.continue(live.handle, continueMessage(spec), {
|
|
2205
|
-
env: this.sessionEnvFor(spec.abPhase, session),
|
|
2256
|
+
env: await this.sessionEnvFor(spec.abPhase, session),
|
|
2206
2257
|
signal,
|
|
2207
2258
|
...(stream !== undefined ? { stream } : {}),
|
|
2208
2259
|
})
|
|
@@ -2215,7 +2266,7 @@ export class BuildRunner {
|
|
|
2215
2266
|
workspacePath,
|
|
2216
2267
|
...(model !== undefined ? { model } : {}),
|
|
2217
2268
|
...(args !== undefined ? { args } : {}),
|
|
2218
|
-
env: this.sessionEnvFor(spec.abPhase, session),
|
|
2269
|
+
env: await this.sessionEnvFor(spec.abPhase, session),
|
|
2219
2270
|
signal,
|
|
2220
2271
|
...(stream !== undefined ? { stream } : {}),
|
|
2221
2272
|
})
|
|
@@ -2560,10 +2611,20 @@ export class BuildRunner {
|
|
|
2560
2611
|
|
|
2561
2612
|
/** D8 ambient auth: sessionEnv may override AB_STORE (remote stores) and
|
|
2562
2613
|
* add AB_TOKEN, but never the per-session identity keys. */
|
|
2563
|
-
private sessionEnvFor(abPhase: string, session: string): Record<string, string
|
|
2614
|
+
private async sessionEnvFor(abPhase: string, session: string): Promise<Record<string, string>> {
|
|
2615
|
+
const parent = this.deps.sessionEnv
|
|
2616
|
+
const token =
|
|
2617
|
+
parent?.AB_STORE && /^https?:/i.test(parent.AB_STORE)
|
|
2618
|
+
? await (this.deps.delegateCredential ?? delegateCredential)(
|
|
2619
|
+
parent.AB_STORE,
|
|
2620
|
+
parent.AB_TOKEN,
|
|
2621
|
+
{ kind: 'build-phase', id: this.deps.slug, session },
|
|
2622
|
+
)
|
|
2623
|
+
: undefined
|
|
2564
2624
|
return {
|
|
2565
2625
|
AB_STORE: 'local',
|
|
2566
2626
|
...this.deps.sessionEnv,
|
|
2627
|
+
...(token ? { AB_TOKEN: token } : {}),
|
|
2567
2628
|
AB_BUILD: this.deps.slug,
|
|
2568
2629
|
AB_PHASE: abPhase,
|
|
2569
2630
|
AB_SESSION: session,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { AbEvent } from '../events/catalog'
|
|
2
|
+
import { DISPATCHER } from '../events/envelope'
|
|
3
|
+
import type { IdSource } from '../ids'
|
|
4
|
+
import { defineReducer } from '../kernel/incremental'
|
|
5
|
+
import type { BuildStore } from '../store/types'
|
|
6
|
+
|
|
7
|
+
interface DispatchBudgetAccumulator {
|
|
8
|
+
failures: Extract<AbEvent, { type: 'dispatch.failed' }>[]
|
|
9
|
+
open: string[]
|
|
10
|
+
aborted: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function dispatchFailureBudgetReducer(infrastructureOwnsWorkspace: boolean) {
|
|
14
|
+
return defineReducer({
|
|
15
|
+
version: 1,
|
|
16
|
+
initial: (): DispatchBudgetAccumulator => ({ failures: [], open: [], aborted: false }),
|
|
17
|
+
fold(acc, events: readonly AbEvent[]) {
|
|
18
|
+
for (const event of events) {
|
|
19
|
+
if (event.type === 'runner.attached') {
|
|
20
|
+
acc.failures = []
|
|
21
|
+
} else if (
|
|
22
|
+
event.type === 'escalation.raised' &&
|
|
23
|
+
event.payload.source === 'policy' &&
|
|
24
|
+
event.payload.policyCause === 'dispatch-failure-limit'
|
|
25
|
+
) {
|
|
26
|
+
if (!acc.open.includes(event.payload.id)) acc.open.push(event.payload.id)
|
|
27
|
+
} else if (event.type === 'escalation.answered' && acc.open.includes(event.payload.id)) {
|
|
28
|
+
acc.open = acc.open.filter((id) => id !== event.payload.id)
|
|
29
|
+
if (event.payload.resolution !== 'abort') acc.failures = []
|
|
30
|
+
else acc.aborted = true
|
|
31
|
+
} else if (
|
|
32
|
+
event.type === 'dispatch.failed' &&
|
|
33
|
+
!(infrastructureOwnsWorkspace && event.payload.stage === 'workspace')
|
|
34
|
+
) {
|
|
35
|
+
acc.failures.push(event)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
finish: (acc) => ({
|
|
40
|
+
count: acc.failures.length,
|
|
41
|
+
latest: acc.failures.at(-1),
|
|
42
|
+
parked: acc.aborted || acc.open.length > 0,
|
|
43
|
+
}),
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Lifetime diagnostic attempt numbers are deliberately independent of this epoch. */
|
|
48
|
+
export function dispatchFailureBudget(
|
|
49
|
+
events: readonly AbEvent[],
|
|
50
|
+
infrastructureOwnsWorkspace: boolean,
|
|
51
|
+
) {
|
|
52
|
+
return dispatchFailureBudgetReducer(infrastructureOwnsWorkspace).reduce(events)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Repair the failure/raise crash gap before allowing another provider call. */
|
|
56
|
+
export async function ensureDispatchBudget(
|
|
57
|
+
store: Pick<BuildStore, 'getEvents' | 'appendIfCurrent'>,
|
|
58
|
+
ids: IdSource,
|
|
59
|
+
slug: string,
|
|
60
|
+
maxAttempts: number,
|
|
61
|
+
infrastructureOwnsWorkspace: boolean,
|
|
62
|
+
): Promise<boolean> {
|
|
63
|
+
for (;;) {
|
|
64
|
+
const events = await store.getEvents(slug)
|
|
65
|
+
const budget = dispatchFailureBudget(events, infrastructureOwnsWorkspace)
|
|
66
|
+
if (budget.parked) return false
|
|
67
|
+
if (budget.count < maxAttempts || budget.latest === undefined) return true
|
|
68
|
+
const failure = budget.latest.payload
|
|
69
|
+
const raised = await store.appendIfCurrent(slug, events.at(-1)?.seq ?? 0, {
|
|
70
|
+
actor: DISPATCHER,
|
|
71
|
+
type: 'escalation.raised',
|
|
72
|
+
payload: {
|
|
73
|
+
id: ids('esc'),
|
|
74
|
+
phase: 'setup',
|
|
75
|
+
source: 'policy',
|
|
76
|
+
policyCause: 'dispatch-failure-limit',
|
|
77
|
+
question: `maxDispatchAttempts (${maxAttempts}) exhausted during ${failure.stage}: ${failure.error}`,
|
|
78
|
+
refs: [],
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
if (raised !== null) return false
|
|
82
|
+
}
|
|
83
|
+
}
|