@defrex/autobuild 0.8.0 → 0.10.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 +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -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/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- 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-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- 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 +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- 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/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- 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 +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -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 +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +254 -73
- 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 +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- 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/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +49 -3
- 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 +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- 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 +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- 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 +204 -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/skills/tickets/SKILL.md +57 -5
- 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,43 @@ 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'
|
|
25
|
+
import { LinearTicketSource } from '../ports/tickets/linear'
|
|
17
26
|
import { inspectLocalStoreSnapshot } from '../store/local/store'
|
|
18
27
|
import type { StoreOpener } from './store-opening'
|
|
19
28
|
import { openProductionStore } from './store-opening'
|
|
20
29
|
import { isRemoteStoreRef, resolveMainRepo, resolveRepoStatePaths } from './repo-state'
|
|
30
|
+
import { createReadinessRedactor, gitText } from './init-readiness-shared'
|
|
21
31
|
|
|
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
|
-
}
|
|
32
|
+
export {
|
|
33
|
+
INIT_PROBE_MARKER,
|
|
34
|
+
createReadinessRedactor,
|
|
35
|
+
gitText,
|
|
36
|
+
parseGuestOutput,
|
|
37
|
+
} from './init-readiness-shared'
|
|
38
|
+
export type {
|
|
39
|
+
GuestProbeReport,
|
|
40
|
+
InitValidationReport,
|
|
41
|
+
ReadinessCheck,
|
|
42
|
+
} from '../ports/workspace/provider-capabilities'
|
|
79
43
|
|
|
80
44
|
async function shell(
|
|
81
45
|
exec: Exec,
|
|
@@ -123,14 +87,38 @@ export async function runGuestReadinessProbe(opts: {
|
|
|
123
87
|
openStore?: StoreOpener
|
|
124
88
|
exec?: Exec
|
|
125
89
|
signal?: AbortSignal
|
|
90
|
+
/**
|
|
91
|
+
* Guest tolerance mode (default `true`): a configured package plugin that
|
|
92
|
+
* cannot be resolved from the candidate roots is skipped with a notice
|
|
93
|
+
* instead of failing readiness. Host-side call sites pass `false` so
|
|
94
|
+
* resolution failures stay fatal.
|
|
95
|
+
*/
|
|
96
|
+
guest?: boolean
|
|
126
97
|
}): Promise<GuestProbeReport> {
|
|
127
98
|
const checks: ReadinessCheck[] = []
|
|
128
99
|
const config = await loadConfig(join(opts.repo, 'autobuild.toml'))
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
100
|
+
// Pre-registry site: plugins are not loaded yet, so provider behavior comes
|
|
101
|
+
// from the builtin capability table (AUT-516).
|
|
102
|
+
const builtinCaps = builtinWorkspaceProviderCapabilities(config.workspace.provider)
|
|
103
|
+
const providerConfig =
|
|
104
|
+
builtinCaps?.configSchema !== undefined
|
|
105
|
+
? builtinCaps.configSchema.parse(config.workspace.config)
|
|
132
106
|
: undefined
|
|
133
|
-
|
|
107
|
+
// Redaction scope is the provider's declared `guestEnvNames` (AUT-539) —
|
|
108
|
+
// for vercel-sandbox that is `environmentVariables` plus
|
|
109
|
+
// `gitUsernameEnv`/`gitPasswordEnv`, deliberately wider than
|
|
110
|
+
// `environmentVariables` alone: the capability contract documents
|
|
111
|
+
// `guestEnvNames` as the readiness-redaction surface, and a git credential
|
|
112
|
+
// echoed into a guest check detail (e.g. a failed clone printing an
|
|
113
|
+
// authenticated URL) is a secret we know the guest holds. Note the
|
|
114
|
+
// byte-level consequence: repos whose git env values appear in a guest
|
|
115
|
+
// check detail now see `[REDACTED]` there. Providers without a
|
|
116
|
+
// `guestEnvNames`/`configSchema` declaration fall back to the name-heuristic
|
|
117
|
+
// redaction only.
|
|
118
|
+
const redact = createReadinessRedactor(
|
|
119
|
+
opts.env,
|
|
120
|
+
providerConfig !== undefined ? (builtinCaps?.guestEnvNames?.(providerConfig) ?? []) : [],
|
|
121
|
+
)
|
|
134
122
|
try {
|
|
135
123
|
const setup = config.commands.setup
|
|
136
124
|
if (opts.signal?.aborted) throw opts.signal.reason ?? new Error('validation cancelled')
|
|
@@ -156,16 +144,54 @@ export async function runGuestReadinessProbe(opts: {
|
|
|
156
144
|
let runtimes: RuntimeRegistry
|
|
157
145
|
try {
|
|
158
146
|
const packageRoot = await resolveMainRepo(opts.repo, opts.exec ?? spawnExec)
|
|
159
|
-
|
|
147
|
+
// This function IS the guest probe (AUT-517): it runs inside the
|
|
148
|
+
// disposable environment and never constructs workspace providers, so a
|
|
149
|
+
// configured provider plugin the guest cannot resolve is skipped with a
|
|
150
|
+
// notice instead of failing readiness. The validateInitReadiness call
|
|
151
|
+
// sites stay strict — the git-worktree branch below passes `guest: false`
|
|
152
|
+
// (AUT-561); the private probe `bin/ab-init-probe.ts` keeps the default
|
|
153
|
+
// guest tolerance. diagnosePlugins shares attemptPlugin with
|
|
154
|
+
// loadPlugins — same resulting registry — but returns per-module reports,
|
|
155
|
+
// so a resolution-stage skip (the guest tolerance path) can be surfaced
|
|
156
|
+
// in the check detail instead of degrading silently.
|
|
157
|
+
const diagnosis = await diagnosePlugins(config.plugins, opts.repo, {
|
|
158
|
+
packageRoot,
|
|
159
|
+
guest: opts.guest ?? true,
|
|
160
|
+
})
|
|
161
|
+
// loadPlugins throws on the first failed report; diagnosePlugins does
|
|
162
|
+
// not, so restore that fail-closed precedence here: repo-path resolution
|
|
163
|
+
// failures and every post-resolution failure (evaluation, manifest,
|
|
164
|
+
// registration) stay fatal for the probe exactly as before, rendered by
|
|
165
|
+
// the surrounding catch into a `configuration and plugins` fail check.
|
|
166
|
+
const failedReport = diagnosis.reports.find((report) => report.status === 'failed')
|
|
167
|
+
if (failedReport !== undefined)
|
|
168
|
+
throw new Error(failedReport.error, { cause: failedReport.cause })
|
|
169
|
+
// Notice parity with loadPlugins: skipped loads were announced on stderr.
|
|
170
|
+
// Only resolution-stage skips alter the check detail; the transitional
|
|
171
|
+
// registration-stage skip (a configured plugin re-registering a builtin
|
|
172
|
+
// workspace provider) is the documented arrangement, not a problem, so it
|
|
173
|
+
// stays out of the report.
|
|
174
|
+
for (const report of diagnosis.reports) {
|
|
175
|
+
if (report.status === 'skipped' && report.notice !== undefined) console.error(report.notice)
|
|
176
|
+
}
|
|
177
|
+
const unresolved = diagnosis.reports.filter(
|
|
178
|
+
(report) => report.status === 'skipped' && report.stage === 'resolution',
|
|
179
|
+
)
|
|
180
|
+
let pluginDetail = `${config.plugins.length} configured plugin(s) loaded`
|
|
181
|
+
for (const report of unresolved) {
|
|
182
|
+
pluginDetail +=
|
|
183
|
+
`; ${report.notice}; to make "${report.module}" available to guests, ` +
|
|
184
|
+
"add the package to the repository's dependencies so it resolves from the repository install during commands.setup"
|
|
185
|
+
}
|
|
160
186
|
runtimes = await materializePluginRuntimes(
|
|
161
187
|
opts.runtimes ?? createProductionRuntimes().runtimes,
|
|
162
|
-
|
|
188
|
+
diagnosis.registry,
|
|
163
189
|
{ repoRoot: opts.repo, env: opts.env },
|
|
164
190
|
)
|
|
165
191
|
checks.push({
|
|
166
192
|
name: 'configuration and plugins',
|
|
167
193
|
status: 'pass',
|
|
168
|
-
detail:
|
|
194
|
+
detail: pluginDetail,
|
|
169
195
|
})
|
|
170
196
|
} catch (error) {
|
|
171
197
|
return {
|
|
@@ -181,7 +207,7 @@ export async function runGuestReadinessProbe(opts: {
|
|
|
181
207
|
}
|
|
182
208
|
|
|
183
209
|
const runtimeRemediation = (runtime: string, preflightOnly: boolean): string =>
|
|
184
|
-
|
|
210
|
+
builtinCaps?.requireRuntimeProvisioning !== true
|
|
185
211
|
? 'install/authenticate this runtime in the local validation environment'
|
|
186
212
|
: `fix workspace.config.runtimeProvisioning.${runtime}${preflightOnly ? '.preflight' : ''} and expose API credential names in workspace.config.environmentVariables`
|
|
187
213
|
|
|
@@ -277,56 +303,79 @@ export async function runGuestReadinessProbe(opts: {
|
|
|
277
303
|
return { checks }
|
|
278
304
|
}
|
|
279
305
|
|
|
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)
|
|
306
|
+
/** Forge and required-environment checks driven by one provider's capability
|
|
307
|
+
* declarations. Shared by the pre-registry builtin preflight and the
|
|
308
|
+
* registry-aware check after plugin load so a plugin provider's declarations
|
|
309
|
+
* are honoured at the same seam. */
|
|
310
|
+
function declaredForgeEnvChecks(
|
|
311
|
+
caps: WorkspaceProviderCapabilities | undefined,
|
|
312
|
+
config: Config,
|
|
313
|
+
env: Record<string, string | undefined>,
|
|
314
|
+
): void {
|
|
315
|
+
if (caps?.supportedForges !== undefined && !caps.supportedForges.includes(config.forge)) {
|
|
301
316
|
throw new Error(
|
|
302
|
-
|
|
317
|
+
caps.forgeValidationMessage ??
|
|
318
|
+
`workspace provider "${config.workspace.provider}" does not support forge "${config.forge}"`,
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
for (const group of caps?.requiredEnv ?? []) {
|
|
322
|
+
if (group.validationMessage === undefined) continue
|
|
323
|
+
const satisfied = group.alternatives.some((names) =>
|
|
324
|
+
names.every((name) => env[name] !== undefined && env[name] !== ''),
|
|
303
325
|
)
|
|
304
|
-
|
|
326
|
+
if (!satisfied) throw new Error(group.validationMessage)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Sandbox-forwarding check driven by one provider's declared
|
|
331
|
+
* `sandboxForbiddenEnv` extras. Enforced at the registry-aware init seam so a
|
|
332
|
+
* plugin-declared forbidden name is refused before any infrastructure is
|
|
333
|
+
* provisioned, mirroring `createWorkspaceProvider`'s construction-site check:
|
|
334
|
+
* same gate (`[orchestrator].enabled`), same name union
|
|
335
|
+
* (`SANDBOX_FORBIDDEN_ENV` plus the declared extras), same message. A no-op
|
|
336
|
+
* repeat for builtins — config parse already rejected every shared and
|
|
337
|
+
* builtin-declared name — and a no-op when the declaration is absent. */
|
|
338
|
+
function declaredSandboxForbiddenEnvCheck(
|
|
339
|
+
caps: WorkspaceProviderCapabilities | undefined,
|
|
340
|
+
config: Config,
|
|
341
|
+
): void {
|
|
342
|
+
if (!config.orchestrator.enabled || caps?.sandboxForbiddenEnv === undefined) return
|
|
343
|
+
for (const name of config.orchestrator.sandbox.environmentVariables) {
|
|
344
|
+
if (SANDBOX_FORBIDDEN_ENV.includes(name) || caps.sandboxForbiddenEnv.includes(name)) {
|
|
345
|
+
throw new Error(sandboxForbiddenEnvMessage(name))
|
|
346
|
+
}
|
|
347
|
+
}
|
|
305
348
|
}
|
|
306
349
|
|
|
307
350
|
function hostPreflight(config: Config, env: Record<string, string | undefined>): void {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
351
|
+
// Pre-registry site: the builtin capability table drives the provider's
|
|
352
|
+
// declared forge and environment requirements (AUT-516). Plugin providers
|
|
353
|
+
// are checked registry-aware after plugin load in validateInitReadiness.
|
|
354
|
+
// The sandbox-forwarding rule is never enforced here: it is enforced at
|
|
355
|
+
// config parse for builtins (plugins are not loaded when config is parsed)
|
|
356
|
+
// and at the two post-load seams — this file's registry-aware check and
|
|
357
|
+
// createWorkspaceProvider construction — for plugin-declared names.
|
|
358
|
+
const caps = builtinWorkspaceProviderCapabilities(config.workspace.provider)
|
|
359
|
+
declaredForgeEnvChecks(caps, config, env)
|
|
360
|
+
// Stays exactly where today's remote-provider branch put it: only a
|
|
361
|
+
// provider with remote readiness preflights its ticket-source credential
|
|
362
|
+
// here, because remote provisioning acquires tickets before allocating
|
|
363
|
+
// disposable infrastructure.
|
|
315
364
|
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)
|
|
365
|
+
caps?.validateReadiness !== undefined &&
|
|
366
|
+
config.tickets.source === 'linear' &&
|
|
367
|
+
!env.LINEAR_API_KEY
|
|
368
|
+
)
|
|
324
369
|
throw new Error('ticket source "linear" requires LINEAR_API_KEY')
|
|
325
370
|
}
|
|
326
371
|
|
|
327
372
|
export async function validateInitReadiness(opts: {
|
|
328
373
|
targetRepo: string
|
|
329
374
|
env: Record<string, string | undefined>
|
|
375
|
+
/** The raw launcher process environment, alongside the dotenv-augmented
|
|
376
|
+
* `env`. When supplied, a declared `processEnvOnly` variable present only in
|
|
377
|
+
* a loaded `.env` fails the registry-aware check below. */
|
|
378
|
+
processEnv?: Record<string, string | undefined>
|
|
330
379
|
stdout?: (line: string) => void
|
|
331
380
|
exec?: Exec
|
|
332
381
|
openStore?: StoreOpener
|
|
@@ -342,20 +391,66 @@ export async function validateInitReadiness(opts: {
|
|
|
342
391
|
const configBytes = await readFile(configPath, 'utf8')
|
|
343
392
|
const config = await loadConfig(configPath)
|
|
344
393
|
const state = resolveRepoStatePaths({ repo, envStore: opts.env.AB_STORE })
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
394
|
+
// Pre-registry parse and redaction come from the builtin capability table;
|
|
395
|
+
// after plugin load the registration's own declarations take over (AUT-516).
|
|
396
|
+
const builtinCaps = builtinWorkspaceProviderCapabilities(config.workspace.provider)
|
|
397
|
+
let providerConfig =
|
|
398
|
+
builtinCaps?.configSchema !== undefined
|
|
399
|
+
? builtinCaps.configSchema.parse(config.workspace.config)
|
|
348
400
|
: undefined
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
401
|
+
let redact = createReadinessRedactor(
|
|
402
|
+
opts.env,
|
|
403
|
+
providerConfig !== undefined ? (builtinCaps?.guestEnvNames?.(providerConfig) ?? []) : [],
|
|
404
|
+
)
|
|
405
|
+
let caps: WorkspaceProviderCapabilities | undefined
|
|
406
|
+
let assigneeLine: string | undefined
|
|
354
407
|
try {
|
|
355
408
|
hostPreflight(config, opts.env)
|
|
356
409
|
// Ticket acquisition is a host responsibility for both workspace providers.
|
|
357
410
|
// Exercise its read surface before allocating disposable infrastructure.
|
|
358
411
|
const hostPlugins = await loadPlugins(config.plugins, repo, { packageRoot: repo })
|
|
412
|
+
caps =
|
|
413
|
+
hostPlugins.workspaceProviders.get(config.workspace.provider)?.capabilities ?? builtinCaps
|
|
414
|
+
// Registry-aware declaration checks. The pre-registry preflight above could
|
|
415
|
+
// only consult the builtin table, so a plugin provider's declared forge and
|
|
416
|
+
// required-environment validations are honoured here (f_9d68b3e5); for the
|
|
417
|
+
// builtins the registration carries the same declarations, so the check is
|
|
418
|
+
// a no-op repeat after the preflight already passed.
|
|
419
|
+
declaredForgeEnvChecks(caps, config, opts.env)
|
|
420
|
+
// Same seam, declared sandbox-forbidden extras (AUT-536): forge/env
|
|
421
|
+
// failures keep today's precedence, ticket acquisition follows.
|
|
422
|
+
declaredSandboxForbiddenEnvCheck(caps, config)
|
|
423
|
+
// The registration's own configSchema and guestEnvNames take over from the
|
|
424
|
+
// builtin table, so a plugin provider's readiness context carries its
|
|
425
|
+
// parsed config and its declared names are redacted (f_55ba7591). A
|
|
426
|
+
// declared guestEnvNames with no declared configSchema still feeds the
|
|
427
|
+
// redactor — it is called with the raw `[workspace.config]` then, mirroring
|
|
428
|
+
// how createWorkspaceProvider falls back to the raw config for
|
|
429
|
+
// requireRuntimeProvisioning (f_1ddf1415). The post-report
|
|
430
|
+
// describeEnvironment call below receives that same raw fallback, so the
|
|
431
|
+
// two call shapes cannot drift apart again.
|
|
432
|
+
if (caps?.configSchema !== undefined)
|
|
433
|
+
providerConfig = caps.configSchema.parse(config.workspace.config)
|
|
434
|
+
if (caps?.guestEnvNames !== undefined)
|
|
435
|
+
redact = createReadinessRedactor(
|
|
436
|
+
opts.env,
|
|
437
|
+
caps.guestEnvNames(providerConfig !== undefined ? providerConfig : config.workspace.config),
|
|
438
|
+
)
|
|
439
|
+
// Registry-aware process-env-only check: a declared name present in the
|
|
440
|
+
// dotenv-augmented `env` but absent from the raw launcher map fails here,
|
|
441
|
+
// so plugin-declared requirements and non-CLI callers are covered too
|
|
442
|
+
// (the CLI path's own guard in main.ts runs before this and keeps its
|
|
443
|
+
// error precedence).
|
|
444
|
+
if (opts.processEnv !== undefined) {
|
|
445
|
+
for (const requirement of caps?.processEnvOnly ?? []) {
|
|
446
|
+
if (
|
|
447
|
+
opts.env[requirement.name] !== undefined &&
|
|
448
|
+
opts.processEnv[requirement.name] === undefined
|
|
449
|
+
) {
|
|
450
|
+
throw new Error(requirement.message)
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
359
454
|
if (config.tickets.source !== 'file') {
|
|
360
455
|
const ticketSource = await createTicketSource(
|
|
361
456
|
config.tickets,
|
|
@@ -367,7 +462,22 @@ export async function validateInitReadiness(opts: {
|
|
|
367
462
|
await ticketSource.listReady({
|
|
368
463
|
...(config.tickets.readyLabels !== undefined ? { labels: config.tickets.readyLabels } : {}),
|
|
369
464
|
state: config.tickets.readyState,
|
|
465
|
+
...(config.tickets.readyAssignee !== undefined
|
|
466
|
+
? { assignee: config.tickets.readyAssignee }
|
|
467
|
+
: {}),
|
|
370
468
|
})
|
|
469
|
+
const assignee = config.tickets.readyAssignee
|
|
470
|
+
if (assignee !== undefined && ticketSource instanceof LinearTicketSource) {
|
|
471
|
+
const user = await ticketSource.resolveAssignee(assignee)
|
|
472
|
+
if (user === null) {
|
|
473
|
+
throw new Error(
|
|
474
|
+
assignee === 'me'
|
|
475
|
+
? '[tickets].readyAssignee = "me" but LINEAR_API_KEY did not resolve to a Linear user — check the key'
|
|
476
|
+
: `[tickets].readyAssignee "${assignee}" matches no user in the Linear workspace — check the address or use "me"`,
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
assigneeLine = `Ticket assignee gate: ${assignee} → ${user.name} <${user.email}>`
|
|
480
|
+
}
|
|
371
481
|
}
|
|
372
482
|
} catch (error) {
|
|
373
483
|
throw new Error(redact(error))
|
|
@@ -399,9 +509,18 @@ export async function validateInitReadiness(opts: {
|
|
|
399
509
|
throw new Error(
|
|
400
510
|
`autobuild.toml differs from committed ${config.baseBranch}; commit setup changes before validating`,
|
|
401
511
|
)
|
|
512
|
+
// The host's strict `loadPlugins(config.plugins, repo, { packageRoot: repo })`
|
|
513
|
+
// above is the operative gate: it already threw on the first unresolvable
|
|
514
|
+
// package plugin before this branch is entered, and it shares the probe's
|
|
515
|
+
// resolution root — `resolveMainRepo` maps a linked worktree back to the
|
|
516
|
+
// main checkout — so `guest: false` cannot change today's outcomes. It is
|
|
517
|
+
// defense in depth that keeps this call site strict per the "host-side
|
|
518
|
+
// sites stay strict" rationale even if that pre-check is ever weakened or
|
|
519
|
+
// reordered (AUT-561).
|
|
402
520
|
const guest = await runGuestReadinessProbe({
|
|
403
521
|
repo: workspace,
|
|
404
522
|
env: { ...opts.env, AB_STORE: state.storeRef },
|
|
523
|
+
guest: false,
|
|
405
524
|
...(opts.openStore !== undefined ? { openStore: opts.openStore } : {}),
|
|
406
525
|
...(opts.runtimes !== undefined ? { runtimes: opts.runtimes } : {}),
|
|
407
526
|
exec,
|
|
@@ -446,103 +565,33 @@ export async function validateInitReadiness(opts: {
|
|
|
446
565
|
}
|
|
447
566
|
if (failure !== undefined) throw failure
|
|
448
567
|
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
|
-
)
|
|
568
|
+
} else if (caps?.validateReadiness !== undefined) {
|
|
569
|
+
// Store preconditions come from the registration's declaration, uniformly
|
|
570
|
+
// for builtin and plugin providers; the texts are byte-identical to the
|
|
571
|
+
// branch they replace.
|
|
572
|
+
if (caps.storeRequirements !== undefined) {
|
|
573
|
+
if (!/^https:\/\//i.test(state.storeRef))
|
|
574
|
+
throw new Error(caps.storeRequirements.storeRefMessage)
|
|
575
|
+
const token = opts.env.AB_TOKEN
|
|
576
|
+
if (!token) throw new Error(caps.storeRequirements.storeTokenMessage)
|
|
467
577
|
}
|
|
468
|
-
await
|
|
469
|
-
|
|
578
|
+
report = await caps.validateReadiness({
|
|
579
|
+
config,
|
|
580
|
+
providerConfig,
|
|
581
|
+
env: opts.env,
|
|
582
|
+
storeRef: state.storeRef,
|
|
583
|
+
storeToken: opts.env.AB_TOKEN ?? '',
|
|
470
584
|
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 }),
|
|
585
|
+
baseBranch: config.baseBranch,
|
|
586
|
+
configBytes,
|
|
587
|
+
exec,
|
|
588
|
+
redact,
|
|
589
|
+
runtimeReferences: effectiveRuntimeReferences(config),
|
|
590
|
+
stdout: opts.stdout ?? (() => {}),
|
|
591
|
+
...(opts.signal !== undefined ? { signal: opts.signal } : {}),
|
|
592
|
+
...(opts.vercelFacade !== undefined ? { facade: opts.vercelFacade } : {}),
|
|
593
|
+
...(opts.packageArchive !== undefined ? { packageArchive: opts.packageArchive } : {}),
|
|
484
594
|
})
|
|
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
595
|
} else {
|
|
547
596
|
throw new Error(
|
|
548
597
|
`workspace provider "${config.workspace.provider}" does not support init readiness validation`,
|
|
@@ -554,20 +603,15 @@ export async function validateInitReadiness(opts: {
|
|
|
554
603
|
stdout(`Readiness: ${report.context} (${report.provider})`)
|
|
555
604
|
stdout(`Store: ${state.storeRef}`)
|
|
556
605
|
stdout(`Forge: ${config.forge}`)
|
|
557
|
-
if (
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
)
|
|
567
|
-
stdout(
|
|
568
|
-
`Runtime provisioning names: ${Object.keys(vercelConfig.runtimeProvisioning).sort().join(', ') || '(none)'}`,
|
|
569
|
-
)
|
|
570
|
-
}
|
|
606
|
+
if (assigneeLine !== undefined) stdout(assigneeLine)
|
|
607
|
+
// Same raw `[workspace.config]` fallback guestEnvNames gets (f_55ba7591): a
|
|
608
|
+
// provider that declares describeEnvironment without a configSchema still
|
|
609
|
+
// receives a defined argument — the raw table — never undefined.
|
|
610
|
+
for (const line of caps?.describeEnvironment?.(
|
|
611
|
+
providerConfig !== undefined ? providerConfig : config.workspace.config,
|
|
612
|
+
opts.env,
|
|
613
|
+
) ?? [])
|
|
614
|
+
stdout(line)
|
|
571
615
|
if (report.workspace !== undefined)
|
|
572
616
|
stdout(
|
|
573
617
|
`Disposable environment: ${report.workspace} (released${report.snapshotsDeleted === undefined ? '' : `; ${report.snapshotsDeleted} snapshot(s) deleted`})`,
|