@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,111 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BuildExecutionHandle,
|
|
3
|
+
BuildExecutionIdentity,
|
|
4
|
+
ExecutionObservation,
|
|
5
|
+
} from './build-execution'
|
|
6
|
+
|
|
7
|
+
/** Guest environment variable carrying the serialized harvest launch
|
|
8
|
+
* envelope, mirroring AB_BUILD_RUNNER_OPTIONS for build children. */
|
|
9
|
+
export const HARVEST_RUNNER_OPTIONS_ENV = 'AB_HARVEST_RUNNER_OPTIONS'
|
|
10
|
+
|
|
11
|
+
/** Where one hosted harvest run executed: the provider-native environment
|
|
12
|
+
* (and session) identity the provider supplies at launch time. */
|
|
13
|
+
export interface HarvestExecutionEnvironment {
|
|
14
|
+
provider: string
|
|
15
|
+
environmentId: string
|
|
16
|
+
sessionId?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Immutable identity passed when a workspace provider is asked to start one
|
|
20
|
+
* hosted harvest execution. Unlike builds there is no durable workspace fact,
|
|
21
|
+
* so the provider resolves the disposable environment itself and reports it
|
|
22
|
+
* on the returned handle's identity; `workspaceRef` is therefore optional and
|
|
23
|
+
* advisory. Run state, config, and outcomes are obtained only through the
|
|
24
|
+
* repository journal in the Store. */
|
|
25
|
+
export interface HarvestExecutionStart {
|
|
26
|
+
storeRef: string
|
|
27
|
+
/** Repository identity (normalized origin) the guest serves. */
|
|
28
|
+
repo: string
|
|
29
|
+
/** Supervisor instance id; doubles as the durable `execution` identity. */
|
|
30
|
+
instance: string
|
|
31
|
+
/** Remote base branch the disposable environment is provisioned from. */
|
|
32
|
+
baseBranch: string
|
|
33
|
+
workspaceRef?: string
|
|
34
|
+
/** Repository-lease holder the guest adopts from the owning dispatch loop:
|
|
35
|
+
* it skips its own claim, heartbeats this holder, and never releases. */
|
|
36
|
+
leaseHolder?: string
|
|
37
|
+
environment?: HarvestExecutionEnvironment
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Serialized private-child envelope. A harvest guest is always owned by its
|
|
41
|
+
* environment — there is no local-parent mode (the guest outlives any host
|
|
42
|
+
* supervision and the environment owns teardown). */
|
|
43
|
+
export type HarvestRunnerLaunch = HarvestExecutionStart & {
|
|
44
|
+
supervision: { kind: 'environment' }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Validate one parsed launch envelope. Invalid/missing envelopes make the
|
|
48
|
+
* guest exit 2 rather than guessing at a Store or lease identity. */
|
|
49
|
+
export function parseHarvestRunnerLaunch(value: unknown): HarvestRunnerLaunch | undefined {
|
|
50
|
+
if (value === null || typeof value !== 'object') return undefined
|
|
51
|
+
const candidate = value as Partial<HarvestRunnerLaunch> & {
|
|
52
|
+
supervision?: Record<string, unknown>
|
|
53
|
+
}
|
|
54
|
+
if (
|
|
55
|
+
typeof candidate.storeRef !== 'string' ||
|
|
56
|
+
candidate.storeRef.length === 0 ||
|
|
57
|
+
typeof candidate.repo !== 'string' ||
|
|
58
|
+
candidate.repo.length === 0 ||
|
|
59
|
+
typeof candidate.instance !== 'string' ||
|
|
60
|
+
candidate.instance.length === 0 ||
|
|
61
|
+
typeof candidate.baseBranch !== 'string' ||
|
|
62
|
+
candidate.baseBranch.length === 0 ||
|
|
63
|
+
candidate.supervision === undefined
|
|
64
|
+
)
|
|
65
|
+
return undefined
|
|
66
|
+
if (candidate.supervision.kind !== 'environment') return undefined
|
|
67
|
+
if (Object.hasOwn(candidate.supervision, 'parentPid')) return undefined
|
|
68
|
+
if (
|
|
69
|
+
candidate.leaseHolder !== undefined &&
|
|
70
|
+
(typeof candidate.leaseHolder !== 'string' || candidate.leaseHolder.length === 0)
|
|
71
|
+
)
|
|
72
|
+
return undefined
|
|
73
|
+
if (
|
|
74
|
+
candidate.workspaceRef !== undefined &&
|
|
75
|
+
(typeof candidate.workspaceRef !== 'string' || candidate.workspaceRef.length === 0)
|
|
76
|
+
)
|
|
77
|
+
return undefined
|
|
78
|
+
const environment = candidate.environment
|
|
79
|
+
if (environment !== undefined) {
|
|
80
|
+
if (
|
|
81
|
+
typeof environment !== 'object' ||
|
|
82
|
+
typeof environment.provider !== 'string' ||
|
|
83
|
+
environment.provider.length === 0 ||
|
|
84
|
+
typeof environment.environmentId !== 'string' ||
|
|
85
|
+
environment.environmentId.length === 0
|
|
86
|
+
)
|
|
87
|
+
return undefined
|
|
88
|
+
if (
|
|
89
|
+
environment.sessionId !== undefined &&
|
|
90
|
+
(typeof environment.sessionId !== 'string' || environment.sessionId.length === 0)
|
|
91
|
+
)
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
return candidate as HarvestRunnerLaunch
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The substitutable seam at the workspace boundary for hosted harvest. A
|
|
98
|
+
* remote provider implements this by provisioning one disposable environment,
|
|
99
|
+
* launching the fixed harvest runner guest beside its checkout, and owning
|
|
100
|
+
* full teardown. Deliberately reuses the build execution handle/identity and
|
|
101
|
+
* observation types so supervision semantics (long-poll wait, teardown
|
|
102
|
+
* outcome, provider liveness) are identical to builds. */
|
|
103
|
+
export interface HarvestExecution {
|
|
104
|
+
start(input: HarvestExecutionStart): Promise<BuildExecutionHandle>
|
|
105
|
+
/** Optional liveness observation of a previously recorded execution, from
|
|
106
|
+
* provider state alone. Absent = this provider cannot observe; callers
|
|
107
|
+
* supervise exactly as before ("cannot observe" never means "act"). */
|
|
108
|
+
observe?(identity: BuildExecutionIdentity): Promise<ExecutionObservation>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type { BuildExecutionHandle, ExecutionObservation }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BuildExecution, BuildExecutionHandle, BuildExecutionStart } from './build-execution'
|
|
2
|
+
|
|
3
|
+
/** Deterministic test double. Production wiring never constructs this class;
|
|
4
|
+
* it keeps exhaustive scripted-agent scenarios in-process behind the same
|
|
5
|
+
* execution seam without weakening process isolation in the product path. */
|
|
6
|
+
export class InProcessBuildExecution implements BuildExecution {
|
|
7
|
+
constructor(private readonly run: (input: BuildExecutionStart) => Promise<void>) {}
|
|
8
|
+
|
|
9
|
+
async start(input: BuildExecutionStart): Promise<BuildExecutionHandle> {
|
|
10
|
+
const completion = Promise.resolve()
|
|
11
|
+
.then(() => this.run(input))
|
|
12
|
+
.then(
|
|
13
|
+
() => ({ exitCode: 0 }),
|
|
14
|
+
() => ({ exitCode: 1 }),
|
|
15
|
+
)
|
|
16
|
+
return {
|
|
17
|
+
identity: { provider: 'in-process', workspaceRef: input.workspaceRef },
|
|
18
|
+
supervision: 'local-parent',
|
|
19
|
+
completion,
|
|
20
|
+
async stop() {
|
|
21
|
+
// Scripted test turns are finite. Cancellation behavior belongs to the
|
|
22
|
+
// real local supervisor tests rather than this deterministic double.
|
|
23
|
+
return { outcome: 'confirmed' }
|
|
24
|
+
},
|
|
25
|
+
async detach() {
|
|
26
|
+
// Scripted test turns are finite and leave nothing to reap.
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { distributionPath } from '../../distribution'
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_PROCESS_GROUP_STOP_TIMEOUT_MS,
|
|
4
|
+
terminateProcessGroup,
|
|
5
|
+
} from '../../processes/process-group'
|
|
6
|
+
import type {
|
|
7
|
+
BuildExecution,
|
|
8
|
+
BuildExecutionExit,
|
|
9
|
+
BuildExecutionHandle,
|
|
10
|
+
BuildExecutionStart,
|
|
11
|
+
} from './build-execution'
|
|
12
|
+
|
|
13
|
+
export const BUILD_RUNNER_OPTIONS_ENV = 'AB_BUILD_RUNNER_OPTIONS'
|
|
14
|
+
|
|
15
|
+
export interface LocalBuildExecutionOptions {
|
|
16
|
+
entrypoint?: string
|
|
17
|
+
env?: Record<string, string | undefined>
|
|
18
|
+
stopTimeoutMs?: number
|
|
19
|
+
spawn?: typeof Bun.spawn
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Shipped local workspace+process implementation. stdio is ignored on
|
|
23
|
+
* purpose: child output can never become a private progress/outcome channel. */
|
|
24
|
+
export class LocalBuildExecution implements BuildExecution {
|
|
25
|
+
private readonly entrypoint: string
|
|
26
|
+
private readonly env: Record<string, string | undefined>
|
|
27
|
+
private readonly stopTimeoutMs: number
|
|
28
|
+
private readonly spawn: typeof Bun.spawn
|
|
29
|
+
|
|
30
|
+
constructor(opts: LocalBuildExecutionOptions = {}) {
|
|
31
|
+
this.entrypoint = opts.entrypoint ?? distributionPath('bin', 'ab-build-runner.ts')
|
|
32
|
+
this.env = opts.env ?? process.env
|
|
33
|
+
this.stopTimeoutMs = opts.stopTimeoutMs ?? DEFAULT_PROCESS_GROUP_STOP_TIMEOUT_MS
|
|
34
|
+
this.spawn = opts.spawn ?? Bun.spawn
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async start(input: BuildExecutionStart): Promise<BuildExecutionHandle> {
|
|
38
|
+
const child = this.spawn([process.execPath, this.entrypoint], {
|
|
39
|
+
env: {
|
|
40
|
+
...this.env,
|
|
41
|
+
[BUILD_RUNNER_OPTIONS_ENV]: JSON.stringify({
|
|
42
|
+
...input,
|
|
43
|
+
supervision: { kind: 'local-parent', parentPid: process.pid },
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
stdin: 'ignore',
|
|
47
|
+
stdout: 'ignore',
|
|
48
|
+
stderr: 'ignore',
|
|
49
|
+
// On POSIX this makes the build child a new session and process-group
|
|
50
|
+
// leader. Every agent tool descendant inherits that group by default.
|
|
51
|
+
detached: true,
|
|
52
|
+
})
|
|
53
|
+
let reaping: Promise<void> | undefined
|
|
54
|
+
const reap = (): Promise<void> =>
|
|
55
|
+
(reaping ??= terminateProcessGroup(child.pid, this.stopTimeoutMs))
|
|
56
|
+
const leaderExit = child.exited.then(
|
|
57
|
+
(exitCode): BuildExecutionExit => ({
|
|
58
|
+
exitCode,
|
|
59
|
+
...(child.signalCode !== null ? { signal: child.signalCode } : {}),
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
const completion = leaderExit.then(async (exit) => {
|
|
63
|
+
// A naturally exited leader may leave inherited descendants behind.
|
|
64
|
+
await reap()
|
|
65
|
+
return exit
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
pid: child.pid,
|
|
70
|
+
supervision: 'local-parent',
|
|
71
|
+
identity: {
|
|
72
|
+
provider: 'local-process',
|
|
73
|
+
workspaceRef: input.workspaceRef,
|
|
74
|
+
environmentId: String(child.pid),
|
|
75
|
+
},
|
|
76
|
+
completion,
|
|
77
|
+
stop: async () => {
|
|
78
|
+
await Promise.all([reap(), leaderExit])
|
|
79
|
+
return { outcome: 'confirmed' }
|
|
80
|
+
},
|
|
81
|
+
// A local execution cannot outlive its parent's teardown: detach is the
|
|
82
|
+
// same full reap as stop (the kernel reaps local children on teardown).
|
|
83
|
+
detach: async () => {
|
|
84
|
+
await Promise.all([reap(), leaderExit])
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operator sandbox capability (AUT-340): one persistent, credential-free
|
|
3
|
+
* environment per operator × repository, provisioned like a build workspace
|
|
4
|
+
* and exposed to agent tools only through the registry. The provider owns
|
|
5
|
+
* environment mechanics; durable facts live in the repository journal; the
|
|
6
|
+
* caller supervises from the journal — the same split as the harvest
|
|
7
|
+
* execution capability.
|
|
8
|
+
*
|
|
9
|
+
* The credential-free rule (SPEC §14): the environment receives no store,
|
|
10
|
+
* forge, ticket-provider, or model credential, so the typed operator tools
|
|
11
|
+
* remain the only route to build state from inside a general-purpose agent
|
|
12
|
+
* shell. `SANDBOX_FORBIDDEN_ENV` is the one name set that rule is enforced
|
|
13
|
+
* from — config validation refuses forwarding any of them, and both adapters
|
|
14
|
+
* build guest command environments from an empty record (never the process
|
|
15
|
+
* environment) that can only ever contain forwarded names plus toolchain
|
|
16
|
+
* PATH. Lexical path validation in the service is not a sandbox boundary;
|
|
17
|
+
* the residual risk is bounded by this rule and by the guest network policy,
|
|
18
|
+
* which is the build workspace's upload-pack policy.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Environment variable names that may never be forwarded into an operator
|
|
22
|
+
* sandbox — store, forge, ticket-provider, model, and Vercel credentials.
|
|
23
|
+
* One set, two enforcers: `[orchestrator].sandbox.environmentVariables`
|
|
24
|
+
* config validation (refuses any of these names) and the guest command
|
|
25
|
+
* environment both adapters build (starts from an empty record and adds only
|
|
26
|
+
* forwarded names plus the toolchain PATH). */
|
|
27
|
+
export const SANDBOX_FORBIDDEN_ENV: readonly string[] = [
|
|
28
|
+
'AB_STORE',
|
|
29
|
+
'AB_TOKEN',
|
|
30
|
+
'GITHUB_TOKEN',
|
|
31
|
+
'GH_TOKEN',
|
|
32
|
+
'LINEAR_API_KEY',
|
|
33
|
+
'VERCEL_OIDC_TOKEN',
|
|
34
|
+
'VERCEL_TOKEN',
|
|
35
|
+
'VERCEL_TEAM_ID',
|
|
36
|
+
'VERCEL_PROJECT_ID',
|
|
37
|
+
'AI_GATEWAY_API_KEY',
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
/** Durable identity of one operator sandbox environment: the provider-scoped
|
|
41
|
+
* name tools re-derive and journal facts carry. Deterministic per
|
|
42
|
+
* (repository origin, operator) — there is at most one live environment per
|
|
43
|
+
* operator and repository. */
|
|
44
|
+
export interface SandboxEnvironmentIdentity {
|
|
45
|
+
provider: string
|
|
46
|
+
/** Provider-scoped durable name (Vercel sandbox name / local worktree path). */
|
|
47
|
+
environmentId: string
|
|
48
|
+
/** Provider session, for journal facts. */
|
|
49
|
+
sessionId?: string
|
|
50
|
+
/** Checkout root inside the environment; tool file paths are rooted here. */
|
|
51
|
+
workspacePath: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SandboxCommandRequest {
|
|
55
|
+
command: string
|
|
56
|
+
cwd?: string
|
|
57
|
+
timeoutSeconds?: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SandboxCommandResult {
|
|
61
|
+
exitCode: number
|
|
62
|
+
stdout: string
|
|
63
|
+
stderr: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** State of one detached command. Vercel reports command output only after
|
|
67
|
+
* exit: while `running`, `stdout`/`stderr` stay undefined. */
|
|
68
|
+
export interface SandboxWaitResult {
|
|
69
|
+
state: 'running' | 'exited'
|
|
70
|
+
exitCode?: number
|
|
71
|
+
stdout?: string
|
|
72
|
+
stderr?: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Typed failure of one operator-sandbox operation. `stage` names where it
|
|
76
|
+
* failed so a tool caller sees `sandbox-<stage>` codes; the message carries
|
|
77
|
+
* the provider's text, redacted of credential values by the service. */
|
|
78
|
+
export class SandboxOperationError extends Error {
|
|
79
|
+
override readonly name = 'SandboxOperationError'
|
|
80
|
+
|
|
81
|
+
constructor(
|
|
82
|
+
readonly stage:
|
|
83
|
+
| 'provision'
|
|
84
|
+
| 'resume'
|
|
85
|
+
| 'exec'
|
|
86
|
+
| 'exec-timeout'
|
|
87
|
+
| 'not-found'
|
|
88
|
+
| 'environment'
|
|
89
|
+
| 'reset'
|
|
90
|
+
| 'release',
|
|
91
|
+
message: string,
|
|
92
|
+
options?: { cause?: unknown },
|
|
93
|
+
) {
|
|
94
|
+
super(message, options)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The substitutable seam at the workspace boundary for operator sandboxes.
|
|
99
|
+
* Every environment-deriving operation takes `{ repo, operator }` (plus
|
|
100
|
+
* `baseBranch` for `ensure`) so the one-environment-per-operator×repository
|
|
101
|
+
* invariant holds at the type level; per-environment operations take the
|
|
102
|
+
* resolved handle. Durable lifecycle facts are journaled by the caller
|
|
103
|
+
* (the sandbox service), never by the provider. */
|
|
104
|
+
export interface OperatorSandboxExecution {
|
|
105
|
+
/** Pure resolution of the deterministic identity — no provisioning, no
|
|
106
|
+
* provider traffic beyond origin discovery. Used by release paths that
|
|
107
|
+
* must name the environment without creating one. */
|
|
108
|
+
describe(input: { repo: string; operator: string }): Promise<SandboxEnvironmentIdentity>
|
|
109
|
+
/** Provision on first use, resume after a stop. `baseBranch` selects the
|
|
110
|
+
* fresh-provision source; reuse/resume never re-checks it. */
|
|
111
|
+
ensure(input: {
|
|
112
|
+
repo: string
|
|
113
|
+
operator: string
|
|
114
|
+
baseBranch: string
|
|
115
|
+
}): Promise<SandboxEnvironmentIdentity>
|
|
116
|
+
exec(
|
|
117
|
+
handle: SandboxEnvironmentIdentity,
|
|
118
|
+
request: SandboxCommandRequest,
|
|
119
|
+
): Promise<SandboxCommandResult>
|
|
120
|
+
start(
|
|
121
|
+
handle: SandboxEnvironmentIdentity,
|
|
122
|
+
request: SandboxCommandRequest,
|
|
123
|
+
): Promise<{ commandId: string }>
|
|
124
|
+
wait(
|
|
125
|
+
handle: SandboxEnvironmentIdentity,
|
|
126
|
+
input: { commandId: string; waitSeconds: number },
|
|
127
|
+
): Promise<SandboxWaitResult>
|
|
128
|
+
readFile(handle: SandboxEnvironmentIdentity, path: string): Promise<Uint8Array>
|
|
129
|
+
writeFile(handle: SandboxEnvironmentIdentity, path: string, content: Uint8Array): Promise<void>
|
|
130
|
+
/** Idle stop: end the session, keep the snapshot. `unsupported` lets local
|
|
131
|
+
* providers decline honestly. Both fields come from journal facts, so the
|
|
132
|
+
* dispatcher can call this without provisioning anything. */
|
|
133
|
+
stop(input: { operator: string; environmentId: string }): Promise<{
|
|
134
|
+
outcome: 'stopped' | 'absent' | 'unsupported'
|
|
135
|
+
}>
|
|
136
|
+
/** Full teardown + snapshot purge, by exact environment identity. */
|
|
137
|
+
release(input: {
|
|
138
|
+
repo: string
|
|
139
|
+
operator: string
|
|
140
|
+
environmentId: string
|
|
141
|
+
}): Promise<{ snapshots: { outcome: 'confirmed' | 'unknown'; deleted?: number; error?: string } }>
|
|
142
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname, isAbsolute, join } from 'node:path'
|
|
3
|
+
import type { Exec } from './git-worktree'
|
|
4
|
+
|
|
5
|
+
export const PHASE_SCRATCH_PREFIX = '.ab/'
|
|
6
|
+
|
|
7
|
+
async function git(exec: Exec, cwd: string, args: string[]): Promise<string> {
|
|
8
|
+
const result = await exec(['git', ...args], { cwd })
|
|
9
|
+
if (result.exitCode !== 0) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`git ${args.join(' ')} exited ${result.exitCode}: ${result.stderr.trim() || result.stdout.trim() || '(no output)'}`,
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
return result.stdout
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Parse Git's -z path output without treating whitespace or quoting specially. */
|
|
18
|
+
export function parseNulPaths(output: string): string[] {
|
|
19
|
+
if (output === '') return []
|
|
20
|
+
if (!output.endsWith('\0')) throw new Error('git path output was not NUL-terminated')
|
|
21
|
+
const paths = output.slice(0, -1).split('\0')
|
|
22
|
+
if (paths.some((path) => path === '')) throw new Error('git path output contained an empty path')
|
|
23
|
+
return paths
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function trackedScratchPaths(
|
|
27
|
+
exec: Exec,
|
|
28
|
+
workspacePath: string,
|
|
29
|
+
commit?: string,
|
|
30
|
+
): Promise<string[]> {
|
|
31
|
+
const args = commit
|
|
32
|
+
? ['ls-tree', '-r', '--name-only', '-z', commit, '--', '.ab']
|
|
33
|
+
: ['ls-files', '-z', '--', '.ab']
|
|
34
|
+
return [...new Set(parseNulPaths(await git(exec, workspacePath, args)))].sort()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Restore the exact index versions removed by a disposable scratch wipe. */
|
|
38
|
+
export async function restoreTrackedScratch(
|
|
39
|
+
exec: Exec,
|
|
40
|
+
workspacePath: string,
|
|
41
|
+
paths: readonly string[],
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
if (paths.length === 0) return
|
|
44
|
+
await git(exec, workspacePath, ['checkout-index', '--force', '--', ...paths])
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Keep generated scratch ignored even when a legacy repo tracks .ab/.gitignore. */
|
|
48
|
+
export async function ensureScratchExcluded(exec: Exec, workspacePath: string): Promise<void> {
|
|
49
|
+
const raw = (await git(exec, workspacePath, ['rev-parse', '--git-path', 'info/exclude'])).trim()
|
|
50
|
+
if (raw === '') throw new Error('git rev-parse returned no exclude path')
|
|
51
|
+
const path = isAbsolute(raw) ? raw : join(workspacePath, raw)
|
|
52
|
+
let content = ''
|
|
53
|
+
try {
|
|
54
|
+
content = await readFile(path, 'utf8')
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
57
|
+
}
|
|
58
|
+
const lines = content.split(/\r?\n/)
|
|
59
|
+
if (lines.includes('.ab/')) return
|
|
60
|
+
await mkdir(dirname(path), { recursive: true })
|
|
61
|
+
const separator = content === '' || content.endsWith('\n') ? '' : '\n'
|
|
62
|
+
await writeFile(path, `${content}${separator}.ab/\n`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function collides(path: string, unavailable: ReadonlySet<string>): boolean {
|
|
66
|
+
for (const other of unavailable) {
|
|
67
|
+
if (path === other || path.startsWith(`${other}/`) || other.startsWith(`${path}/`)) return true
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Allocate a stable path relative to .ab without overwriting tracked content. */
|
|
73
|
+
export function allocateScratchPath(
|
|
74
|
+
desired: string,
|
|
75
|
+
tracked: ReadonlySet<string>,
|
|
76
|
+
allocated: Set<string>,
|
|
77
|
+
): string {
|
|
78
|
+
const unavailable = new Set([...tracked, ...allocated])
|
|
79
|
+
const conventional = `${PHASE_SCRATCH_PREFIX}${desired}`
|
|
80
|
+
if (!collides(conventional, unavailable)) {
|
|
81
|
+
allocated.add(conventional)
|
|
82
|
+
return desired
|
|
83
|
+
}
|
|
84
|
+
for (let index = 1; ; index += 1) {
|
|
85
|
+
const candidate = `generated/${index}/${desired}`
|
|
86
|
+
if (!collides(`${PHASE_SCRATCH_PREFIX}${candidate}`, unavailable)) {
|
|
87
|
+
allocated.add(`${PHASE_SCRATCH_PREFIX}${candidate}`)
|
|
88
|
+
return candidate
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function changedScratchPaths(
|
|
94
|
+
exec: Exec,
|
|
95
|
+
workspacePath: string,
|
|
96
|
+
from: string,
|
|
97
|
+
to: string,
|
|
98
|
+
): Promise<Set<string>> {
|
|
99
|
+
return new Set(
|
|
100
|
+
parseNulPaths(
|
|
101
|
+
await git(exec, workspacePath, ['diff', '--name-only', '-z', from, to, '--', '.ab']),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Every scratch path touched by any commit, including changes later reverted. */
|
|
107
|
+
export async function scratchPathsTouchedInRange(
|
|
108
|
+
exec: Exec,
|
|
109
|
+
workspacePath: string,
|
|
110
|
+
base: string,
|
|
111
|
+
head: string,
|
|
112
|
+
): Promise<string[]> {
|
|
113
|
+
const commits = (await git(exec, workspacePath, ['rev-list', '--reverse', `${base}..${head}`]))
|
|
114
|
+
.trim()
|
|
115
|
+
.split('\n')
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
const touched = new Set<string>()
|
|
118
|
+
for (const commit of commits) {
|
|
119
|
+
const parents = (await git(exec, workspacePath, ['rev-list', '--parents', '-n', '1', commit]))
|
|
120
|
+
.trim()
|
|
121
|
+
.split(/\s+/)
|
|
122
|
+
.slice(1)
|
|
123
|
+
for (const parent of parents) {
|
|
124
|
+
for (const path of await changedScratchPaths(exec, workspacePath, parent, commit)) {
|
|
125
|
+
touched.add(path)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [...touched].sort()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Merge-authored scratch differs from every parent; inherited bytes are safe. */
|
|
133
|
+
export async function reconcileScratchViolations(
|
|
134
|
+
exec: Exec,
|
|
135
|
+
workspacePath: string,
|
|
136
|
+
head: string,
|
|
137
|
+
parents: readonly string[],
|
|
138
|
+
): Promise<string[]> {
|
|
139
|
+
let intersection: Set<string> | undefined
|
|
140
|
+
for (const parent of parents) {
|
|
141
|
+
const changed = await changedScratchPaths(exec, workspacePath, parent, head)
|
|
142
|
+
intersection =
|
|
143
|
+
intersection === undefined
|
|
144
|
+
? changed
|
|
145
|
+
: new Set([...intersection].filter((path) => changed.has(path)))
|
|
146
|
+
}
|
|
147
|
+
return [...(intersection ?? [])].sort()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function phaseScratchRejection(paths: readonly string[]): Error {
|
|
151
|
+
return new Error(
|
|
152
|
+
`publication rejected: commits must not add, modify, or delete paths under .ab/:\n` +
|
|
153
|
+
`${paths.map((path) => ` ${path}`).join('\n')}\n` +
|
|
154
|
+
'Unstage or drop the .ab/ change, amend the commit if needed, then retry completion.',
|
|
155
|
+
)
|
|
156
|
+
}
|