@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,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FakeWorkspaceProvider (SPEC §3.2): WorkspaceProvider for seam tests. Its
|
|
3
|
+
* default filesystem mode copies a source working tree so the returned path
|
|
4
|
+
* has the same usable-path semantics as GitWorktreeProvider. High-volume
|
|
5
|
+
* process-decision tests may explicitly select `logical` mode when their
|
|
6
|
+
* synthetic repo/path values are intentionally not filesystem fixtures.
|
|
7
|
+
*
|
|
8
|
+
* Shape parity with GitWorktreeProvider: `ref` and `path` are the same
|
|
9
|
+
* string (`<root>/<branch>`), provision is idempotent per branch (resume is
|
|
10
|
+
* a re-run, not a special path — constitution #2), and release of an
|
|
11
|
+
* unknown or already-released workspace is a no-op, never an error.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
14
|
+
import {
|
|
15
|
+
cp,
|
|
16
|
+
mkdir,
|
|
17
|
+
readFile as fsReadFile,
|
|
18
|
+
rm,
|
|
19
|
+
stat,
|
|
20
|
+
writeFile as fsWriteFile,
|
|
21
|
+
} from 'node:fs/promises'
|
|
22
|
+
import { dirname, join, resolve, sep } from 'node:path'
|
|
23
|
+
import type { WorkspaceBase } from '../../ontology'
|
|
24
|
+
import {
|
|
25
|
+
SandboxOperationError,
|
|
26
|
+
type OperatorSandboxExecution,
|
|
27
|
+
type SandboxCommandResult,
|
|
28
|
+
type SandboxEnvironmentIdentity,
|
|
29
|
+
type SandboxWaitResult,
|
|
30
|
+
} from './operator-sandbox'
|
|
31
|
+
import type { WorkspaceHandle, WorkspaceProvider, WorkspaceProvisionResult } from '../types'
|
|
32
|
+
|
|
33
|
+
export interface ProvisionRecord {
|
|
34
|
+
repo: string
|
|
35
|
+
baseBranch: string
|
|
36
|
+
branch: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type FakeWorkspaceMode = 'filesystem' | 'logical'
|
|
40
|
+
|
|
41
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
42
|
+
try {
|
|
43
|
+
await stat(path)
|
|
44
|
+
return true
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false
|
|
47
|
+
throw error
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class FakeWorkspaceProvider implements WorkspaceProvider {
|
|
52
|
+
readonly name = 'fake'
|
|
53
|
+
readonly orchestratorSandbox: OperatorSandboxExecution
|
|
54
|
+
|
|
55
|
+
/** Journals — public so tests assert directly on call order and args. */
|
|
56
|
+
readonly provisions: ProvisionRecord[] = []
|
|
57
|
+
readonly releases: WorkspaceHandle[] = []
|
|
58
|
+
|
|
59
|
+
private readonly root: string
|
|
60
|
+
private readonly initialBase: WorkspaceBase
|
|
61
|
+
private readonly mode: FakeWorkspaceMode
|
|
62
|
+
private readonly sandboxRoot: string
|
|
63
|
+
private readonly sandboxSetupCommand: string | undefined
|
|
64
|
+
private readonly sandboxEnvSource: Record<string, string | undefined>
|
|
65
|
+
private readonly sandboxEnvironmentVariables: readonly string[]
|
|
66
|
+
/** ref → handle for workspaces provisioned and not yet released. */
|
|
67
|
+
private readonly active = new Map<string, WorkspaceHandle>()
|
|
68
|
+
/** Durable fake branch heads survive release, like real Git branches. */
|
|
69
|
+
private readonly branchHeads = new Map<string, string>()
|
|
70
|
+
private readonly failures = new Map<'provision' | 'release', Error>()
|
|
71
|
+
|
|
72
|
+
constructor(
|
|
73
|
+
opts: {
|
|
74
|
+
root?: string
|
|
75
|
+
base?: WorkspaceBase
|
|
76
|
+
/** Default `filesystem` enforces a real usable working-copy path. */
|
|
77
|
+
mode?: FakeWorkspaceMode
|
|
78
|
+
/** Operator-sandbox knobs: sandbox root (defaults to a sibling of the
|
|
79
|
+
* workspace root), the setup command, and forwarded variable names
|
|
80
|
+
* resolved from `envSource`. */
|
|
81
|
+
sandboxRoot?: string
|
|
82
|
+
sandboxSetupCommand?: string
|
|
83
|
+
sandboxEnvironmentVariables?: readonly string[]
|
|
84
|
+
envSource?: Record<string, string | undefined>
|
|
85
|
+
} = {},
|
|
86
|
+
) {
|
|
87
|
+
this.root = resolve(opts.root ?? '/fake/workspaces')
|
|
88
|
+
this.initialBase = opts.base ?? { source: 'remote', sha: 'fake-base-sha' }
|
|
89
|
+
this.mode = opts.mode ?? 'filesystem'
|
|
90
|
+
this.sandboxRoot = resolve(opts.sandboxRoot ?? join(this.root, '..', 'orchestrator-sandboxes'))
|
|
91
|
+
this.sandboxSetupCommand = opts.sandboxSetupCommand
|
|
92
|
+
this.sandboxEnvironmentVariables = opts.sandboxEnvironmentVariables ?? []
|
|
93
|
+
this.sandboxEnvSource = opts.envSource ?? {}
|
|
94
|
+
this.orchestratorSandbox = {
|
|
95
|
+
describe: (input) => Promise.resolve(this.sandboxIdentity(input.repo, input.operator)),
|
|
96
|
+
ensure: (input) => this.ensureSandbox(input),
|
|
97
|
+
exec: (handle, request) => this.sandboxExec(handle, request),
|
|
98
|
+
start: (handle, request) => this.sandboxStart(handle, request),
|
|
99
|
+
wait: (handle, input) => this.sandboxWait(handle, input),
|
|
100
|
+
readFile: (handle, path) => this.sandboxReadFile(handle, path),
|
|
101
|
+
writeFile: (handle, path, content) => this.sandboxWriteFile(handle, path, content),
|
|
102
|
+
stop: () => Promise.resolve({ outcome: 'stopped' as const }),
|
|
103
|
+
release: (input) => this.sandboxRelease(input),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Injectable failure: while set, the named operation throws `error` on
|
|
109
|
+
* every call (pass `null` to clear). Lets tests drive the provision/release
|
|
110
|
+
* failure paths without a real provider.
|
|
111
|
+
*/
|
|
112
|
+
setFailure(op: 'provision' | 'release', error: Error | null): void {
|
|
113
|
+
if (error === null) this.failures.delete(op)
|
|
114
|
+
else this.failures.set(op, error)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Whether the workspace at `ref` is currently provisioned. */
|
|
118
|
+
isActive(ref: string): boolean {
|
|
119
|
+
return this.active.has(ref)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Test seam for commits made between provision calls. */
|
|
123
|
+
setBranchHead(branch: string, sha: string): void {
|
|
124
|
+
this.branchHeads.set(branch, sha)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async provision(opts: {
|
|
128
|
+
repo: string
|
|
129
|
+
baseBranch: string
|
|
130
|
+
branch: string
|
|
131
|
+
}): Promise<WorkspaceProvisionResult> {
|
|
132
|
+
const failure = this.failures.get('provision')
|
|
133
|
+
if (failure) throw failure
|
|
134
|
+
const ref = resolve(join(this.root, opts.branch))
|
|
135
|
+
const existing = this.active.get(ref)
|
|
136
|
+
if (existing) {
|
|
137
|
+
if (this.mode === 'logical' || (await pathExists(existing.path))) {
|
|
138
|
+
this.provisions.push({ ...opts })
|
|
139
|
+
return {
|
|
140
|
+
...existing,
|
|
141
|
+
base: {
|
|
142
|
+
source: 'existing',
|
|
143
|
+
sha: this.branchHeads.get(opts.branch) ?? this.initialBase.sha,
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// The active map is only process-local bookkeeping. If its filesystem
|
|
148
|
+
// working copy disappeared out of band, forget that stale registration
|
|
149
|
+
// and rematerialize below without changing the durable fake branch head.
|
|
150
|
+
this.active.delete(ref)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (this.mode === 'filesystem') {
|
|
154
|
+
await mkdir(dirname(ref), { recursive: true })
|
|
155
|
+
// The fake owns its root. Remove an out-of-band leftover before making
|
|
156
|
+
// the new active working copy, just as worktree prune permits recovery.
|
|
157
|
+
await rm(ref, { recursive: true, force: true })
|
|
158
|
+
await cp(opts.repo, ref, { recursive: true })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const handle: WorkspaceHandle = {
|
|
162
|
+
provider: this.name,
|
|
163
|
+
ref,
|
|
164
|
+
path: ref,
|
|
165
|
+
branch: opts.branch,
|
|
166
|
+
}
|
|
167
|
+
this.active.set(ref, handle)
|
|
168
|
+
this.provisions.push({ ...opts })
|
|
169
|
+
|
|
170
|
+
const existingSha = this.branchHeads.get(opts.branch)
|
|
171
|
+
if (existingSha !== undefined) {
|
|
172
|
+
return { ...handle, base: { source: 'existing', sha: existingSha } }
|
|
173
|
+
}
|
|
174
|
+
this.branchHeads.set(opts.branch, this.initialBase.sha)
|
|
175
|
+
return { ...handle, base: { ...this.initialBase } }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Idempotent: releasing an unknown or already-released handle is a no-op
|
|
179
|
+
* (matching GitWorktreeProvider's already-gone-worktree behavior). */
|
|
180
|
+
async release(handle: WorkspaceHandle): Promise<void> {
|
|
181
|
+
const failure = this.failures.get('release')
|
|
182
|
+
if (failure) throw failure
|
|
183
|
+
this.releases.push({
|
|
184
|
+
provider: handle.provider,
|
|
185
|
+
ref: handle.ref,
|
|
186
|
+
path: handle.path,
|
|
187
|
+
branch: handle.branch,
|
|
188
|
+
})
|
|
189
|
+
const active = this.active.get(handle.ref)
|
|
190
|
+
if (!active) return
|
|
191
|
+
if (this.mode === 'filesystem') {
|
|
192
|
+
await rm(active.path, { recursive: true, force: true })
|
|
193
|
+
}
|
|
194
|
+
this.active.delete(handle.ref)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Operator sandbox capability (AUT-340) ───────────────────
|
|
198
|
+
// Filesystem-backed so service and registry contract tests exercise real
|
|
199
|
+
// exec/read/write/reset semantics against a disposable tree.
|
|
200
|
+
|
|
201
|
+
/** Journal — environments idly stopped via the capability's `stop`. */
|
|
202
|
+
readonly sandboxStops: Array<{ operator: string; environmentId: string }> = []
|
|
203
|
+
/** Journal — full teardowns via the capability's `release`. */
|
|
204
|
+
readonly sandboxReleases: Array<{ repo: string; operator: string; environmentId: string }> = []
|
|
205
|
+
/** Serialization probe: in-flight exec recording, for overlap tests. */
|
|
206
|
+
private readonly sandboxChildren = new Map<
|
|
207
|
+
string,
|
|
208
|
+
{ proc: ReturnType<typeof Bun.spawn>; stdout: string; stderr: string; exitCode: number | null }
|
|
209
|
+
>()
|
|
210
|
+
|
|
211
|
+
private sandboxIdentity(repo: string, operator: string): SandboxEnvironmentIdentity {
|
|
212
|
+
const digest = createHash('sha256').update(`${repo}\0${operator}`).digest('hex').slice(0, 10)
|
|
213
|
+
const path = join(this.sandboxRoot, digest)
|
|
214
|
+
return { provider: this.name, environmentId: path, workspacePath: path }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The credential-free guest environment: empty record + host PATH + the
|
|
218
|
+
* forwarded names. Mirrors the builtin adapters' rule. */
|
|
219
|
+
private sandboxEnv(): Record<string, string> {
|
|
220
|
+
const env: Record<string, string> = {
|
|
221
|
+
PATH: this.sandboxEnvSource.PATH ?? '/usr/local/bin:/usr/bin:/bin',
|
|
222
|
+
}
|
|
223
|
+
for (const name of this.sandboxEnvironmentVariables) {
|
|
224
|
+
const value = this.sandboxEnvSource[name]
|
|
225
|
+
if (value === undefined || value === '') {
|
|
226
|
+
throw new SandboxOperationError(
|
|
227
|
+
'environment',
|
|
228
|
+
`operator sandbox environment variable ${name} is not set in the host environment`,
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
env[name] = value
|
|
232
|
+
}
|
|
233
|
+
return env
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private async ensureSandbox(input: {
|
|
237
|
+
repo: string
|
|
238
|
+
operator: string
|
|
239
|
+
baseBranch: string
|
|
240
|
+
}): Promise<SandboxEnvironmentIdentity> {
|
|
241
|
+
const identity = this.sandboxIdentity(input.repo, input.operator)
|
|
242
|
+
if (await pathExists(join(identity.workspacePath, '.autobuild-sandbox-provisioned'))) {
|
|
243
|
+
return identity
|
|
244
|
+
}
|
|
245
|
+
if (this.mode !== 'filesystem') {
|
|
246
|
+
throw new SandboxOperationError(
|
|
247
|
+
'provision',
|
|
248
|
+
'the logical fake mode has no filesystem to host an operator sandbox',
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
await mkdir(dirname(identity.workspacePath), { recursive: true })
|
|
252
|
+
await rm(identity.workspacePath, { recursive: true, force: true })
|
|
253
|
+
await cp(input.repo, identity.workspacePath, { recursive: true })
|
|
254
|
+
try {
|
|
255
|
+
if (this.sandboxSetupCommand !== undefined && this.sandboxSetupCommand.trim() !== '') {
|
|
256
|
+
const proc = Bun.spawn(['sh', '-c', this.sandboxSetupCommand], {
|
|
257
|
+
cwd: identity.workspacePath,
|
|
258
|
+
stdin: 'ignore',
|
|
259
|
+
stdout: 'pipe',
|
|
260
|
+
stderr: 'pipe',
|
|
261
|
+
env: this.sandboxEnv(),
|
|
262
|
+
})
|
|
263
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
264
|
+
new Response(proc.stdout).text(),
|
|
265
|
+
new Response(proc.stderr).text(),
|
|
266
|
+
proc.exited,
|
|
267
|
+
])
|
|
268
|
+
if (exitCode !== 0) {
|
|
269
|
+
throw new SandboxOperationError(
|
|
270
|
+
'provision',
|
|
271
|
+
`operator sandbox setup failed: ${stderr.trim() || stdout.trim() || `exit ${exitCode}`}`,
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
await fsWriteFile(join(identity.workspacePath, '.autobuild-sandbox-provisioned'), '')
|
|
276
|
+
} catch (error) {
|
|
277
|
+
await rm(identity.workspacePath, { recursive: true, force: true })
|
|
278
|
+
throw error
|
|
279
|
+
}
|
|
280
|
+
return identity
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private sandboxCwd(handle: SandboxEnvironmentIdentity, cwd: string | undefined): string {
|
|
284
|
+
return cwd === undefined ? handle.workspacePath : join(handle.workspacePath, cwd)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async sandboxExec(
|
|
288
|
+
handle: SandboxEnvironmentIdentity,
|
|
289
|
+
request: { command: string; cwd?: string; timeoutSeconds?: number },
|
|
290
|
+
): Promise<SandboxCommandResult> {
|
|
291
|
+
const timeoutSeconds = Math.min(Math.max(request.timeoutSeconds ?? 120, 1), 300)
|
|
292
|
+
const proc = Bun.spawn(['sh', '-c', request.command], {
|
|
293
|
+
cwd: this.sandboxCwd(handle, request.cwd),
|
|
294
|
+
stdin: 'ignore',
|
|
295
|
+
stdout: 'pipe',
|
|
296
|
+
stderr: 'pipe',
|
|
297
|
+
env: this.sandboxEnv(),
|
|
298
|
+
})
|
|
299
|
+
const timer = AbortSignal.timeout(timeoutSeconds * 1000)
|
|
300
|
+
const outcome = await Promise.race([
|
|
301
|
+
proc.exited.then((code) => ({ kind: 'exited' as const, code })),
|
|
302
|
+
new Promise<never>((_, reject) => {
|
|
303
|
+
timer.addEventListener(
|
|
304
|
+
'abort',
|
|
305
|
+
() =>
|
|
306
|
+
reject(
|
|
307
|
+
new SandboxOperationError(
|
|
308
|
+
'exec-timeout',
|
|
309
|
+
`sandbox command exceeded its ${timeoutSeconds}s bound and was killed`,
|
|
310
|
+
),
|
|
311
|
+
),
|
|
312
|
+
{ once: true },
|
|
313
|
+
)
|
|
314
|
+
}),
|
|
315
|
+
]).catch((error: unknown) => {
|
|
316
|
+
try {
|
|
317
|
+
proc.kill('SIGTERM')
|
|
318
|
+
} catch {
|
|
319
|
+
/* already exited */
|
|
320
|
+
}
|
|
321
|
+
throw error
|
|
322
|
+
})
|
|
323
|
+
const [stdout, stderr] = await Promise.all([
|
|
324
|
+
new Response(proc.stdout).text(),
|
|
325
|
+
new Response(proc.stderr).text(),
|
|
326
|
+
])
|
|
327
|
+
return { exitCode: outcome.code ?? -1, stdout, stderr }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private async sandboxStart(
|
|
331
|
+
handle: SandboxEnvironmentIdentity,
|
|
332
|
+
request: { command: string; cwd?: string },
|
|
333
|
+
): Promise<{ commandId: string }> {
|
|
334
|
+
const proc = Bun.spawn(['sh', '-c', request.command], {
|
|
335
|
+
cwd: this.sandboxCwd(handle, request.cwd),
|
|
336
|
+
stdin: 'ignore',
|
|
337
|
+
stdout: 'pipe',
|
|
338
|
+
stderr: 'pipe',
|
|
339
|
+
env: this.sandboxEnv(),
|
|
340
|
+
})
|
|
341
|
+
const commandId = `sbcmd-${Math.random().toString(36).slice(2, 12)}`
|
|
342
|
+
const tracked = { proc, stdout: '', stderr: '', exitCode: null as number | null }
|
|
343
|
+
this.sandboxChildren.set(commandId, tracked)
|
|
344
|
+
void (async () => {
|
|
345
|
+
const decoder = new TextDecoder()
|
|
346
|
+
const reader = proc.stdout.getReader()
|
|
347
|
+
for (;;) {
|
|
348
|
+
const { done, value } = await reader.read()
|
|
349
|
+
if (done) break
|
|
350
|
+
tracked.stdout += decoder.decode(value, { stream: true })
|
|
351
|
+
}
|
|
352
|
+
})()
|
|
353
|
+
void (async () => {
|
|
354
|
+
const decoder = new TextDecoder()
|
|
355
|
+
const reader = proc.stderr.getReader()
|
|
356
|
+
for (;;) {
|
|
357
|
+
const { done, value } = await reader.read()
|
|
358
|
+
if (done) break
|
|
359
|
+
tracked.stderr += decoder.decode(value, { stream: true })
|
|
360
|
+
}
|
|
361
|
+
})()
|
|
362
|
+
void proc.exited.then((code) => {
|
|
363
|
+
tracked.exitCode = code ?? -1
|
|
364
|
+
})
|
|
365
|
+
return { commandId }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private async sandboxWait(
|
|
369
|
+
_handle: SandboxEnvironmentIdentity,
|
|
370
|
+
input: { commandId: string; waitSeconds: number },
|
|
371
|
+
): Promise<SandboxWaitResult> {
|
|
372
|
+
const child = this.sandboxChildren.get(input.commandId)
|
|
373
|
+
if (child === undefined) {
|
|
374
|
+
throw new SandboxOperationError(
|
|
375
|
+
'not-found',
|
|
376
|
+
`unknown sandbox command ${JSON.stringify(input.commandId)}`,
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
const deadline = Date.now() + Math.min(Math.max(input.waitSeconds, 0), 300) * 1000
|
|
380
|
+
while (child.exitCode === null && Date.now() < deadline) {
|
|
381
|
+
await new Promise((resolve) => setTimeout(resolve, 25))
|
|
382
|
+
}
|
|
383
|
+
if (child.exitCode === null) {
|
|
384
|
+
return { state: 'running', stdout: child.stdout, stderr: child.stderr }
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
state: 'exited',
|
|
388
|
+
exitCode: child.exitCode,
|
|
389
|
+
stdout: child.stdout,
|
|
390
|
+
stderr: child.stderr,
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private async sandboxReadFile(
|
|
395
|
+
handle: SandboxEnvironmentIdentity,
|
|
396
|
+
path: string,
|
|
397
|
+
): Promise<Uint8Array> {
|
|
398
|
+
return new Uint8Array(await fsReadFile(this.sandboxRootedPath(handle, path)))
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private async sandboxWriteFile(
|
|
402
|
+
handle: SandboxEnvironmentIdentity,
|
|
403
|
+
path: string,
|
|
404
|
+
content: Uint8Array,
|
|
405
|
+
): Promise<void> {
|
|
406
|
+
const rooted = this.sandboxRootedPath(handle, path)
|
|
407
|
+
await mkdir(dirname(rooted), { recursive: true })
|
|
408
|
+
await fsWriteFile(rooted, content)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Root tool paths at the checkout; rejects escapes lexically. */
|
|
412
|
+
private sandboxRootedPath(handle: SandboxEnvironmentIdentity, path: string): string {
|
|
413
|
+
const rooted = resolve(handle.workspacePath, path)
|
|
414
|
+
if (!rooted.startsWith(resolve(handle.workspacePath) + sep)) {
|
|
415
|
+
throw new SandboxOperationError(
|
|
416
|
+
'exec',
|
|
417
|
+
`sandbox path ${JSON.stringify(path)} escapes the checkout`,
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
return rooted
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private async sandboxRelease(input: {
|
|
424
|
+
repo: string
|
|
425
|
+
operator: string
|
|
426
|
+
environmentId: string
|
|
427
|
+
}): Promise<{ snapshots: { outcome: 'confirmed' } }> {
|
|
428
|
+
this.sandboxReleases.push({ ...input })
|
|
429
|
+
await rm(input.environmentId, { recursive: true, force: true })
|
|
430
|
+
return { snapshots: { outcome: 'confirmed' } }
|
|
431
|
+
}
|
|
432
|
+
}
|