@defrex/autobuild 0.8.0 → 0.9.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 +1 -1
- package/SPEC.md +313 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +57 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +151 -25
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +169 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +10 -0
- package/package.json +43 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/model.ts +57 -7
- package/packages/core/src/cli/dashboard/poll.ts +69 -10
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch.ts +320 -46
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +47 -8
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +248 -222
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/status.ts +149 -27
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +307 -7
- package/packages/core/src/cli/wait.ts +147 -39
- package/packages/core/src/cli/watch.ts +128 -166
- package/packages/core/src/config/live.ts +22 -0
- package/packages/core/src/config/pipeline-source.ts +376 -0
- package/packages/core/src/config/schema.ts +230 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +2 -1
- package/packages/core/src/kernel/auto-merge-default.ts +123 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/reducer.ts +40 -1
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +208 -19
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/types.ts +17 -2
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +37 -0
- package/packages/core/src/processes/build-runner.ts +54 -3
- package/packages/core/src/processes/dispatcher.ts +247 -41
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/orchestrator-tick.ts +319 -0
- package/packages/core/src/processes/sandbox-state.ts +10 -0
- package/packages/core/src/store/adapter.ts +32 -0
- package/packages/core/src/store/build-scope.ts +64 -0
- package/packages/core/src/store/contract.ts +868 -0
- package/packages/core/src/store/digest.ts +49 -0
- package/packages/core/src/store/local/schema.ts +29 -2
- package/packages/core/src/store/local/store.ts +338 -4
- package/packages/core/src/store/memory.ts +179 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +71 -0
- package/packages/core/src/store/remote/client.ts +260 -1
- package/packages/core/src/store/remote/index.ts +12 -2
- package/packages/core/src/store/remote/protocol.ts +100 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +140 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +64 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +28 -9
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +425 -0
- package/packages/core/src/store/types.ts +122 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +126 -33
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +169 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -3,79 +3,42 @@ import { tmpdir } from 'node:os'
|
|
|
3
3
|
import { join, resolve } from 'node:path'
|
|
4
4
|
import { effectiveRuntimeReferences } from '../config/roles'
|
|
5
5
|
import type { Config } from '../config/schema'
|
|
6
|
-
import { vercelSandboxConfigSchema } from '../config/schema'
|
|
7
6
|
import { loadConfig } from '../config/load'
|
|
8
7
|
import { createProductionRuntimes } from '../ports/runner/production'
|
|
9
8
|
import { createRuntimeResolver } from '../ports/runner/routing'
|
|
10
9
|
import type { RuntimeRegistry } from '../ports/runner/runtime'
|
|
11
10
|
import type { Exec } from '../ports/workspace/git-worktree'
|
|
12
11
|
import { spawnExec } from '../ports/workspace/git-worktree'
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { builtinWorkspaceProviderCapabilities } from '../ports/workspace/builtin-capabilities'
|
|
13
|
+
import type {
|
|
14
|
+
GuestProbeReport,
|
|
15
|
+
InitValidationReport,
|
|
16
|
+
ReadinessCheck,
|
|
17
|
+
WorkspaceProviderCapabilities,
|
|
18
|
+
} from '../ports/workspace/provider-capabilities'
|
|
19
|
+
import { sandboxForbiddenEnvMessage } from '../ports/workspace/provider-capabilities'
|
|
20
|
+
import { SANDBOX_FORBIDDEN_ENV } from '../ports/workspace/operator-sandbox'
|
|
21
|
+
import type { VercelSandboxFacade } from '../ports/workspace/vercel-sandbox'
|
|
22
|
+
import { diagnosePlugins, loadPlugins } from '../plugins/load'
|
|
15
23
|
import { materializePluginRuntimes } from '../plugins/runtimes'
|
|
16
24
|
import { createTicketSource } from '../ports/tickets/create'
|
|
17
25
|
import { inspectLocalStoreSnapshot } from '../store/local/store'
|
|
18
26
|
import type { StoreOpener } from './store-opening'
|
|
19
27
|
import { openProductionStore } from './store-opening'
|
|
20
28
|
import { isRemoteStoreRef, resolveMainRepo, resolveRepoStatePaths } from './repo-state'
|
|
29
|
+
import { createReadinessRedactor, gitText } from './init-readiness-shared'
|
|
21
30
|
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
workspace?: string
|
|
34
|
-
revision?: string
|
|
35
|
-
checks: ReadinessCheck[]
|
|
36
|
-
exitCode: number
|
|
37
|
-
/** Vercel only: automatic snapshots deleted while releasing the disposable
|
|
38
|
-
* environment; zero proves no snapshot storage was left behind. */
|
|
39
|
-
snapshotsDeleted?: number
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface GuestProbeReport {
|
|
43
|
-
checks: ReadinessCheck[]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function message(error: unknown): string {
|
|
47
|
-
if (error instanceof AggregateError) {
|
|
48
|
-
return [error.message, ...error.errors.map(message)].filter(Boolean).join('; ')
|
|
49
|
-
}
|
|
50
|
-
if (error instanceof Error)
|
|
51
|
-
return `${error.message}${error.cause === undefined ? '' : `; ${message(error.cause)}`}`
|
|
52
|
-
return String(error)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Replace every supplied nonempty value, longest first, in all diagnostics. */
|
|
56
|
-
export function createReadinessRedactor(
|
|
57
|
-
env: Readonly<Record<string, string | undefined>>,
|
|
58
|
-
explicitSecretNames: readonly string[] = [],
|
|
59
|
-
): (value: unknown) => string {
|
|
60
|
-
const namedSecrets = new Set(explicitSecretNames)
|
|
61
|
-
const secrets = [
|
|
62
|
-
...new Set(
|
|
63
|
-
Object.entries(env)
|
|
64
|
-
.filter(
|
|
65
|
-
([name, value]) =>
|
|
66
|
-
value !== undefined &&
|
|
67
|
-
value !== '' &&
|
|
68
|
-
(namedSecrets.has(name) || /(?:TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|AUTH)/i.test(name)),
|
|
69
|
-
)
|
|
70
|
-
.map(([, value]) => value as string),
|
|
71
|
-
),
|
|
72
|
-
].sort((left, right) => right.length - left.length)
|
|
73
|
-
return (value) => {
|
|
74
|
-
let text = message(value)
|
|
75
|
-
for (const secret of secrets) text = text.split(secret).join('[REDACTED]')
|
|
76
|
-
return text
|
|
77
|
-
}
|
|
78
|
-
}
|
|
31
|
+
export {
|
|
32
|
+
INIT_PROBE_MARKER,
|
|
33
|
+
createReadinessRedactor,
|
|
34
|
+
gitText,
|
|
35
|
+
parseGuestOutput,
|
|
36
|
+
} from './init-readiness-shared'
|
|
37
|
+
export type {
|
|
38
|
+
GuestProbeReport,
|
|
39
|
+
InitValidationReport,
|
|
40
|
+
ReadinessCheck,
|
|
41
|
+
} from '../ports/workspace/provider-capabilities'
|
|
79
42
|
|
|
80
43
|
async function shell(
|
|
81
44
|
exec: Exec,
|
|
@@ -123,14 +86,38 @@ export async function runGuestReadinessProbe(opts: {
|
|
|
123
86
|
openStore?: StoreOpener
|
|
124
87
|
exec?: Exec
|
|
125
88
|
signal?: AbortSignal
|
|
89
|
+
/**
|
|
90
|
+
* Guest tolerance mode (default `true`): a configured package plugin that
|
|
91
|
+
* cannot be resolved from the candidate roots is skipped with a notice
|
|
92
|
+
* instead of failing readiness. Host-side call sites pass `false` so
|
|
93
|
+
* resolution failures stay fatal.
|
|
94
|
+
*/
|
|
95
|
+
guest?: boolean
|
|
126
96
|
}): Promise<GuestProbeReport> {
|
|
127
97
|
const checks: ReadinessCheck[] = []
|
|
128
98
|
const config = await loadConfig(join(opts.repo, 'autobuild.toml'))
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
99
|
+
// Pre-registry site: plugins are not loaded yet, so provider behavior comes
|
|
100
|
+
// from the builtin capability table (AUT-516).
|
|
101
|
+
const builtinCaps = builtinWorkspaceProviderCapabilities(config.workspace.provider)
|
|
102
|
+
const providerConfig =
|
|
103
|
+
builtinCaps?.configSchema !== undefined
|
|
104
|
+
? builtinCaps.configSchema.parse(config.workspace.config)
|
|
132
105
|
: undefined
|
|
133
|
-
|
|
106
|
+
// Redaction scope is the provider's declared `guestEnvNames` (AUT-539) —
|
|
107
|
+
// for vercel-sandbox that is `environmentVariables` plus
|
|
108
|
+
// `gitUsernameEnv`/`gitPasswordEnv`, deliberately wider than
|
|
109
|
+
// `environmentVariables` alone: the capability contract documents
|
|
110
|
+
// `guestEnvNames` as the readiness-redaction surface, and a git credential
|
|
111
|
+
// echoed into a guest check detail (e.g. a failed clone printing an
|
|
112
|
+
// authenticated URL) is a secret we know the guest holds. Note the
|
|
113
|
+
// byte-level consequence: repos whose git env values appear in a guest
|
|
114
|
+
// check detail now see `[REDACTED]` there. Providers without a
|
|
115
|
+
// `guestEnvNames`/`configSchema` declaration fall back to the name-heuristic
|
|
116
|
+
// redaction only.
|
|
117
|
+
const redact = createReadinessRedactor(
|
|
118
|
+
opts.env,
|
|
119
|
+
providerConfig !== undefined ? (builtinCaps?.guestEnvNames?.(providerConfig) ?? []) : [],
|
|
120
|
+
)
|
|
134
121
|
try {
|
|
135
122
|
const setup = config.commands.setup
|
|
136
123
|
if (opts.signal?.aborted) throw opts.signal.reason ?? new Error('validation cancelled')
|
|
@@ -156,16 +143,54 @@ export async function runGuestReadinessProbe(opts: {
|
|
|
156
143
|
let runtimes: RuntimeRegistry
|
|
157
144
|
try {
|
|
158
145
|
const packageRoot = await resolveMainRepo(opts.repo, opts.exec ?? spawnExec)
|
|
159
|
-
|
|
146
|
+
// This function IS the guest probe (AUT-517): it runs inside the
|
|
147
|
+
// disposable environment and never constructs workspace providers, so a
|
|
148
|
+
// configured provider plugin the guest cannot resolve is skipped with a
|
|
149
|
+
// notice instead of failing readiness. The validateInitReadiness call
|
|
150
|
+
// sites stay strict — the git-worktree branch below passes `guest: false`
|
|
151
|
+
// (AUT-561); the private probe `bin/ab-init-probe.ts` keeps the default
|
|
152
|
+
// guest tolerance. diagnosePlugins shares attemptPlugin with
|
|
153
|
+
// loadPlugins — same resulting registry — but returns per-module reports,
|
|
154
|
+
// so a resolution-stage skip (the guest tolerance path) can be surfaced
|
|
155
|
+
// in the check detail instead of degrading silently.
|
|
156
|
+
const diagnosis = await diagnosePlugins(config.plugins, opts.repo, {
|
|
157
|
+
packageRoot,
|
|
158
|
+
guest: opts.guest ?? true,
|
|
159
|
+
})
|
|
160
|
+
// loadPlugins throws on the first failed report; diagnosePlugins does
|
|
161
|
+
// not, so restore that fail-closed precedence here: repo-path resolution
|
|
162
|
+
// failures and every post-resolution failure (evaluation, manifest,
|
|
163
|
+
// registration) stay fatal for the probe exactly as before, rendered by
|
|
164
|
+
// the surrounding catch into a `configuration and plugins` fail check.
|
|
165
|
+
const failedReport = diagnosis.reports.find((report) => report.status === 'failed')
|
|
166
|
+
if (failedReport !== undefined)
|
|
167
|
+
throw new Error(failedReport.error, { cause: failedReport.cause })
|
|
168
|
+
// Notice parity with loadPlugins: skipped loads were announced on stderr.
|
|
169
|
+
// Only resolution-stage skips alter the check detail; the transitional
|
|
170
|
+
// registration-stage skip (a configured plugin re-registering a builtin
|
|
171
|
+
// workspace provider) is the documented arrangement, not a problem, so it
|
|
172
|
+
// stays out of the report.
|
|
173
|
+
for (const report of diagnosis.reports) {
|
|
174
|
+
if (report.status === 'skipped' && report.notice !== undefined) console.error(report.notice)
|
|
175
|
+
}
|
|
176
|
+
const unresolved = diagnosis.reports.filter(
|
|
177
|
+
(report) => report.status === 'skipped' && report.stage === 'resolution',
|
|
178
|
+
)
|
|
179
|
+
let pluginDetail = `${config.plugins.length} configured plugin(s) loaded`
|
|
180
|
+
for (const report of unresolved) {
|
|
181
|
+
pluginDetail +=
|
|
182
|
+
`; ${report.notice}; to make "${report.module}" available to guests, ` +
|
|
183
|
+
"add the package to the repository's dependencies so it resolves from the repository install during commands.setup"
|
|
184
|
+
}
|
|
160
185
|
runtimes = await materializePluginRuntimes(
|
|
161
186
|
opts.runtimes ?? createProductionRuntimes().runtimes,
|
|
162
|
-
|
|
187
|
+
diagnosis.registry,
|
|
163
188
|
{ repoRoot: opts.repo, env: opts.env },
|
|
164
189
|
)
|
|
165
190
|
checks.push({
|
|
166
191
|
name: 'configuration and plugins',
|
|
167
192
|
status: 'pass',
|
|
168
|
-
detail:
|
|
193
|
+
detail: pluginDetail,
|
|
169
194
|
})
|
|
170
195
|
} catch (error) {
|
|
171
196
|
return {
|
|
@@ -181,7 +206,7 @@ export async function runGuestReadinessProbe(opts: {
|
|
|
181
206
|
}
|
|
182
207
|
|
|
183
208
|
const runtimeRemediation = (runtime: string, preflightOnly: boolean): string =>
|
|
184
|
-
|
|
209
|
+
builtinCaps?.requireRuntimeProvisioning !== true
|
|
185
210
|
? 'install/authenticate this runtime in the local validation environment'
|
|
186
211
|
: `fix workspace.config.runtimeProvisioning.${runtime}${preflightOnly ? '.preflight' : ''} and expose API credential names in workspace.config.environmentVariables`
|
|
187
212
|
|
|
@@ -277,56 +302,79 @@ export async function runGuestReadinessProbe(opts: {
|
|
|
277
302
|
return { checks }
|
|
278
303
|
}
|
|
279
304
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
async function gitText(
|
|
291
|
-
exec: Exec,
|
|
292
|
-
repo: string,
|
|
293
|
-
args: string[],
|
|
294
|
-
signal?: AbortSignal,
|
|
295
|
-
): Promise<string> {
|
|
296
|
-
const result = await exec(['git', ...args], {
|
|
297
|
-
cwd: repo,
|
|
298
|
-
...(signal === undefined ? {} : { signal }),
|
|
299
|
-
})
|
|
300
|
-
if (result.exitCode !== 0)
|
|
305
|
+
/** Forge and required-environment checks driven by one provider's capability
|
|
306
|
+
* declarations. Shared by the pre-registry builtin preflight and the
|
|
307
|
+
* registry-aware check after plugin load so a plugin provider's declarations
|
|
308
|
+
* are honoured at the same seam. */
|
|
309
|
+
function declaredForgeEnvChecks(
|
|
310
|
+
caps: WorkspaceProviderCapabilities | undefined,
|
|
311
|
+
config: Config,
|
|
312
|
+
env: Record<string, string | undefined>,
|
|
313
|
+
): void {
|
|
314
|
+
if (caps?.supportedForges !== undefined && !caps.supportedForges.includes(config.forge)) {
|
|
301
315
|
throw new Error(
|
|
302
|
-
|
|
316
|
+
caps.forgeValidationMessage ??
|
|
317
|
+
`workspace provider "${config.workspace.provider}" does not support forge "${config.forge}"`,
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
for (const group of caps?.requiredEnv ?? []) {
|
|
321
|
+
if (group.validationMessage === undefined) continue
|
|
322
|
+
const satisfied = group.alternatives.some((names) =>
|
|
323
|
+
names.every((name) => env[name] !== undefined && env[name] !== ''),
|
|
303
324
|
)
|
|
304
|
-
|
|
325
|
+
if (!satisfied) throw new Error(group.validationMessage)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Sandbox-forwarding check driven by one provider's declared
|
|
330
|
+
* `sandboxForbiddenEnv` extras. Enforced at the registry-aware init seam so a
|
|
331
|
+
* plugin-declared forbidden name is refused before any infrastructure is
|
|
332
|
+
* provisioned, mirroring `createWorkspaceProvider`'s construction-site check:
|
|
333
|
+
* same gate (`[orchestrator].enabled`), same name union
|
|
334
|
+
* (`SANDBOX_FORBIDDEN_ENV` plus the declared extras), same message. A no-op
|
|
335
|
+
* repeat for builtins — config parse already rejected every shared and
|
|
336
|
+
* builtin-declared name — and a no-op when the declaration is absent. */
|
|
337
|
+
function declaredSandboxForbiddenEnvCheck(
|
|
338
|
+
caps: WorkspaceProviderCapabilities | undefined,
|
|
339
|
+
config: Config,
|
|
340
|
+
): void {
|
|
341
|
+
if (!config.orchestrator.enabled || caps?.sandboxForbiddenEnv === undefined) return
|
|
342
|
+
for (const name of config.orchestrator.sandbox.environmentVariables) {
|
|
343
|
+
if (SANDBOX_FORBIDDEN_ENV.includes(name) || caps.sandboxForbiddenEnv.includes(name)) {
|
|
344
|
+
throw new Error(sandboxForbiddenEnvMessage(name))
|
|
345
|
+
}
|
|
346
|
+
}
|
|
305
347
|
}
|
|
306
348
|
|
|
307
349
|
function hostPreflight(config: Config, env: Record<string, string | undefined>): void {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
350
|
+
// Pre-registry site: the builtin capability table drives the provider's
|
|
351
|
+
// declared forge and environment requirements (AUT-516). Plugin providers
|
|
352
|
+
// are checked registry-aware after plugin load in validateInitReadiness.
|
|
353
|
+
// The sandbox-forwarding rule is never enforced here: it is enforced at
|
|
354
|
+
// config parse for builtins (plugins are not loaded when config is parsed)
|
|
355
|
+
// and at the two post-load seams — this file's registry-aware check and
|
|
356
|
+
// createWorkspaceProvider construction — for plugin-declared names.
|
|
357
|
+
const caps = builtinWorkspaceProviderCapabilities(config.workspace.provider)
|
|
358
|
+
declaredForgeEnvChecks(caps, config, env)
|
|
359
|
+
// Stays exactly where today's remote-provider branch put it: only a
|
|
360
|
+
// provider with remote readiness preflights its ticket-source credential
|
|
361
|
+
// here, because remote provisioning acquires tickets before allocating
|
|
362
|
+
// disposable infrastructure.
|
|
315
363
|
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
'Vercel authentication requires VERCEL_OIDC_TOKEN or the durable VERCEL_TOKEN, VERCEL_TEAM_ID, and VERCEL_PROJECT_ID set',
|
|
321
|
-
)
|
|
322
|
-
}
|
|
323
|
-
if (config.tickets.source === 'linear' && !env.LINEAR_API_KEY)
|
|
364
|
+
caps?.validateReadiness !== undefined &&
|
|
365
|
+
config.tickets.source === 'linear' &&
|
|
366
|
+
!env.LINEAR_API_KEY
|
|
367
|
+
)
|
|
324
368
|
throw new Error('ticket source "linear" requires LINEAR_API_KEY')
|
|
325
369
|
}
|
|
326
370
|
|
|
327
371
|
export async function validateInitReadiness(opts: {
|
|
328
372
|
targetRepo: string
|
|
329
373
|
env: Record<string, string | undefined>
|
|
374
|
+
/** The raw launcher process environment, alongside the dotenv-augmented
|
|
375
|
+
* `env`. When supplied, a declared `processEnvOnly` variable present only in
|
|
376
|
+
* a loaded `.env` fails the registry-aware check below. */
|
|
377
|
+
processEnv?: Record<string, string | undefined>
|
|
330
378
|
stdout?: (line: string) => void
|
|
331
379
|
exec?: Exec
|
|
332
380
|
openStore?: StoreOpener
|
|
@@ -342,20 +390,65 @@ export async function validateInitReadiness(opts: {
|
|
|
342
390
|
const configBytes = await readFile(configPath, 'utf8')
|
|
343
391
|
const config = await loadConfig(configPath)
|
|
344
392
|
const state = resolveRepoStatePaths({ repo, envStore: opts.env.AB_STORE })
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
393
|
+
// Pre-registry parse and redaction come from the builtin capability table;
|
|
394
|
+
// after plugin load the registration's own declarations take over (AUT-516).
|
|
395
|
+
const builtinCaps = builtinWorkspaceProviderCapabilities(config.workspace.provider)
|
|
396
|
+
let providerConfig =
|
|
397
|
+
builtinCaps?.configSchema !== undefined
|
|
398
|
+
? builtinCaps.configSchema.parse(config.workspace.config)
|
|
348
399
|
: undefined
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
400
|
+
let redact = createReadinessRedactor(
|
|
401
|
+
opts.env,
|
|
402
|
+
providerConfig !== undefined ? (builtinCaps?.guestEnvNames?.(providerConfig) ?? []) : [],
|
|
403
|
+
)
|
|
404
|
+
let caps: WorkspaceProviderCapabilities | undefined
|
|
354
405
|
try {
|
|
355
406
|
hostPreflight(config, opts.env)
|
|
356
407
|
// Ticket acquisition is a host responsibility for both workspace providers.
|
|
357
408
|
// Exercise its read surface before allocating disposable infrastructure.
|
|
358
409
|
const hostPlugins = await loadPlugins(config.plugins, repo, { packageRoot: repo })
|
|
410
|
+
caps =
|
|
411
|
+
hostPlugins.workspaceProviders.get(config.workspace.provider)?.capabilities ?? builtinCaps
|
|
412
|
+
// Registry-aware declaration checks. The pre-registry preflight above could
|
|
413
|
+
// only consult the builtin table, so a plugin provider's declared forge and
|
|
414
|
+
// required-environment validations are honoured here (f_9d68b3e5); for the
|
|
415
|
+
// builtins the registration carries the same declarations, so the check is
|
|
416
|
+
// a no-op repeat after the preflight already passed.
|
|
417
|
+
declaredForgeEnvChecks(caps, config, opts.env)
|
|
418
|
+
// Same seam, declared sandbox-forbidden extras (AUT-536): forge/env
|
|
419
|
+
// failures keep today's precedence, ticket acquisition follows.
|
|
420
|
+
declaredSandboxForbiddenEnvCheck(caps, config)
|
|
421
|
+
// The registration's own configSchema and guestEnvNames take over from the
|
|
422
|
+
// builtin table, so a plugin provider's readiness context carries its
|
|
423
|
+
// parsed config and its declared names are redacted (f_55ba7591). A
|
|
424
|
+
// declared guestEnvNames with no declared configSchema still feeds the
|
|
425
|
+
// redactor — it is called with the raw `[workspace.config]` then, mirroring
|
|
426
|
+
// how createWorkspaceProvider falls back to the raw config for
|
|
427
|
+
// requireRuntimeProvisioning (f_1ddf1415). The post-report
|
|
428
|
+
// describeEnvironment call below receives that same raw fallback, so the
|
|
429
|
+
// two call shapes cannot drift apart again.
|
|
430
|
+
if (caps?.configSchema !== undefined)
|
|
431
|
+
providerConfig = caps.configSchema.parse(config.workspace.config)
|
|
432
|
+
if (caps?.guestEnvNames !== undefined)
|
|
433
|
+
redact = createReadinessRedactor(
|
|
434
|
+
opts.env,
|
|
435
|
+
caps.guestEnvNames(providerConfig !== undefined ? providerConfig : config.workspace.config),
|
|
436
|
+
)
|
|
437
|
+
// Registry-aware process-env-only check: a declared name present in the
|
|
438
|
+
// dotenv-augmented `env` but absent from the raw launcher map fails here,
|
|
439
|
+
// so plugin-declared requirements and non-CLI callers are covered too
|
|
440
|
+
// (the CLI path's own guard in main.ts runs before this and keeps its
|
|
441
|
+
// error precedence).
|
|
442
|
+
if (opts.processEnv !== undefined) {
|
|
443
|
+
for (const requirement of caps?.processEnvOnly ?? []) {
|
|
444
|
+
if (
|
|
445
|
+
opts.env[requirement.name] !== undefined &&
|
|
446
|
+
opts.processEnv[requirement.name] === undefined
|
|
447
|
+
) {
|
|
448
|
+
throw new Error(requirement.message)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
359
452
|
if (config.tickets.source !== 'file') {
|
|
360
453
|
const ticketSource = await createTicketSource(
|
|
361
454
|
config.tickets,
|
|
@@ -399,9 +492,18 @@ export async function validateInitReadiness(opts: {
|
|
|
399
492
|
throw new Error(
|
|
400
493
|
`autobuild.toml differs from committed ${config.baseBranch}; commit setup changes before validating`,
|
|
401
494
|
)
|
|
495
|
+
// The host's strict `loadPlugins(config.plugins, repo, { packageRoot: repo })`
|
|
496
|
+
// above is the operative gate: it already threw on the first unresolvable
|
|
497
|
+
// package plugin before this branch is entered, and it shares the probe's
|
|
498
|
+
// resolution root — `resolveMainRepo` maps a linked worktree back to the
|
|
499
|
+
// main checkout — so `guest: false` cannot change today's outcomes. It is
|
|
500
|
+
// defense in depth that keeps this call site strict per the "host-side
|
|
501
|
+
// sites stay strict" rationale even if that pre-check is ever weakened or
|
|
502
|
+
// reordered (AUT-561).
|
|
402
503
|
const guest = await runGuestReadinessProbe({
|
|
403
504
|
repo: workspace,
|
|
404
505
|
env: { ...opts.env, AB_STORE: state.storeRef },
|
|
506
|
+
guest: false,
|
|
405
507
|
...(opts.openStore !== undefined ? { openStore: opts.openStore } : {}),
|
|
406
508
|
...(opts.runtimes !== undefined ? { runtimes: opts.runtimes } : {}),
|
|
407
509
|
exec,
|
|
@@ -446,103 +548,33 @@ export async function validateInitReadiness(opts: {
|
|
|
446
548
|
}
|
|
447
549
|
if (failure !== undefined) throw failure
|
|
448
550
|
if (cleanupFailure !== undefined) throw cleanupFailure
|
|
449
|
-
} else if (
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
repo,
|
|
459
|
-
['ls-remote', '--heads', 'origin', `refs/heads/${config.baseBranch}`],
|
|
460
|
-
opts.signal,
|
|
461
|
-
)
|
|
462
|
-
const remoteRevision = remoteLine.split(/\s+/)[0]
|
|
463
|
-
if (!/^[0-9a-f]{40,64}$/i.test(remoteRevision ?? '')) {
|
|
464
|
-
throw new Error(
|
|
465
|
-
`remote base ${config.baseBranch} does not exist; commit and push setup changes before validating`,
|
|
466
|
-
)
|
|
551
|
+
} else if (caps?.validateReadiness !== undefined) {
|
|
552
|
+
// Store preconditions come from the registration's declaration, uniformly
|
|
553
|
+
// for builtin and plugin providers; the texts are byte-identical to the
|
|
554
|
+
// branch they replace.
|
|
555
|
+
if (caps.storeRequirements !== undefined) {
|
|
556
|
+
if (!/^https:\/\//i.test(state.storeRef))
|
|
557
|
+
throw new Error(caps.storeRequirements.storeRefMessage)
|
|
558
|
+
const token = opts.env.AB_TOKEN
|
|
559
|
+
if (!token) throw new Error(caps.storeRequirements.storeTokenMessage)
|
|
467
560
|
}
|
|
468
|
-
await
|
|
469
|
-
|
|
561
|
+
report = await caps.validateReadiness({
|
|
562
|
+
config,
|
|
563
|
+
providerConfig,
|
|
564
|
+
env: opts.env,
|
|
565
|
+
storeRef: state.storeRef,
|
|
566
|
+
storeToken: opts.env.AB_TOKEN ?? '',
|
|
470
567
|
repo,
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
opts.
|
|
480
|
-
)
|
|
481
|
-
const shownConfig = await exec(['git', 'show', `${remoteRevision}:autobuild.toml`], {
|
|
482
|
-
cwd: repo,
|
|
483
|
-
...(opts.signal === undefined ? {} : { signal: opts.signal }),
|
|
568
|
+
baseBranch: config.baseBranch,
|
|
569
|
+
configBytes,
|
|
570
|
+
exec,
|
|
571
|
+
redact,
|
|
572
|
+
runtimeReferences: effectiveRuntimeReferences(config),
|
|
573
|
+
stdout: opts.stdout ?? (() => {}),
|
|
574
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
575
|
+
...(opts.vercelFacade !== undefined ? { facade: opts.vercelFacade } : {}),
|
|
576
|
+
...(opts.packageArchive !== undefined ? { packageArchive: opts.packageArchive } : {}),
|
|
484
577
|
})
|
|
485
|
-
if (shownConfig.exitCode !== 0) {
|
|
486
|
-
throw new Error(
|
|
487
|
-
`remote ${config.baseBranch} does not contain autobuild.toml; commit and push setup changes before validating`,
|
|
488
|
-
)
|
|
489
|
-
}
|
|
490
|
-
if (shownConfig.stdout !== configBytes) {
|
|
491
|
-
throw new Error(
|
|
492
|
-
`remote ${config.baseBranch} autobuild.toml differs from this checkout; commit and push setup changes before validating`,
|
|
493
|
-
)
|
|
494
|
-
}
|
|
495
|
-
let remote: Awaited<ReturnType<typeof validateVercelSandbox>>
|
|
496
|
-
try {
|
|
497
|
-
remote = await validateVercelSandbox({
|
|
498
|
-
config: vercel,
|
|
499
|
-
env: opts.env,
|
|
500
|
-
storeRef,
|
|
501
|
-
storeToken: token,
|
|
502
|
-
repo,
|
|
503
|
-
baseBranch: config.baseBranch,
|
|
504
|
-
...(opts.vercelFacade !== undefined ? { facade: opts.vercelFacade } : {}),
|
|
505
|
-
exec,
|
|
506
|
-
...(opts.packageArchive !== undefined ? { packageArchive: opts.packageArchive } : {}),
|
|
507
|
-
runtimeReferences: effectiveRuntimeReferences(config),
|
|
508
|
-
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
509
|
-
onSandbox: (name) => opts.stdout?.(`Disposable Vercel Sandbox: ${name} (active)`),
|
|
510
|
-
})
|
|
511
|
-
} catch (error) {
|
|
512
|
-
throw new Error(redact(error))
|
|
513
|
-
}
|
|
514
|
-
let guest: GuestProbeReport
|
|
515
|
-
try {
|
|
516
|
-
guest = parseGuestOutput(remote.output)
|
|
517
|
-
} catch (error) {
|
|
518
|
-
throw new Error(
|
|
519
|
-
`disposable sandbox ${remote.sandbox} was deleted, but its readiness output was invalid: ${redact(error)}`,
|
|
520
|
-
)
|
|
521
|
-
}
|
|
522
|
-
report = {
|
|
523
|
-
provider: 'vercel-sandbox',
|
|
524
|
-
context: 'Vercel Sandbox',
|
|
525
|
-
workspace: remote.sandbox,
|
|
526
|
-
revision: remote.revision,
|
|
527
|
-
snapshotsDeleted: remote.snapshotsDeleted,
|
|
528
|
-
checks: [
|
|
529
|
-
{
|
|
530
|
-
name: 'repository acquisition',
|
|
531
|
-
status: 'pass',
|
|
532
|
-
detail: `${remote.origin} ${remote.revision}`,
|
|
533
|
-
},
|
|
534
|
-
{
|
|
535
|
-
name: 'system provisioning',
|
|
536
|
-
status: 'pass',
|
|
537
|
-
detail:
|
|
538
|
-
remote.provisioning.length === 0
|
|
539
|
-
? 'no workspace.config.provisioning steps declared'
|
|
540
|
-
: `completed: ${remote.provisioning.join(', ')}`,
|
|
541
|
-
},
|
|
542
|
-
...guest.checks,
|
|
543
|
-
],
|
|
544
|
-
exitCode: guest.checks.some((check) => check.status === 'fail') ? 1 : 0,
|
|
545
|
-
}
|
|
546
578
|
} else {
|
|
547
579
|
throw new Error(
|
|
548
580
|
`workspace provider "${config.workspace.provider}" does not support init readiness validation`,
|
|
@@ -554,20 +586,14 @@ export async function validateInitReadiness(opts: {
|
|
|
554
586
|
stdout(`Readiness: ${report.context} (${report.provider})`)
|
|
555
587
|
stdout(`Store: ${state.storeRef}`)
|
|
556
588
|
stdout(`Forge: ${config.forge}`)
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
stdout(
|
|
565
|
-
`Guest environment variable names: ${vercelConfig.environmentVariables.join(', ') || '(none)'}`,
|
|
566
|
-
)
|
|
567
|
-
stdout(
|
|
568
|
-
`Runtime provisioning names: ${Object.keys(vercelConfig.runtimeProvisioning).sort().join(', ') || '(none)'}`,
|
|
569
|
-
)
|
|
570
|
-
}
|
|
589
|
+
// Same raw `[workspace.config]` fallback guestEnvNames gets (f_55ba7591): a
|
|
590
|
+
// provider that declares describeEnvironment without a configSchema still
|
|
591
|
+
// receives a defined argument — the raw table — never undefined.
|
|
592
|
+
for (const line of caps?.describeEnvironment?.(
|
|
593
|
+
providerConfig !== undefined ? providerConfig : config.workspace.config,
|
|
594
|
+
opts.env,
|
|
595
|
+
) ?? [])
|
|
596
|
+
stdout(line)
|
|
571
597
|
if (report.workspace !== undefined)
|
|
572
598
|
stdout(
|
|
573
599
|
`Disposable environment: ${report.workspace} (released${report.snapshotsDeleted === undefined ? '' : `; ${report.snapshotsDeleted} snapshot(s) deleted`})`,
|