@cat-factory/server 0.313.0 → 0.314.1
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/dist/agents/ContainerRepoBootstrapper.d.ts +121 -8
- package/dist/agents/ContainerRepoBootstrapper.d.ts.map +1 -1
- package/dist/agents/ContainerRepoBootstrapper.js +257 -45
- package/dist/agents/ContainerRepoBootstrapper.js.map +1 -1
- package/dist/agents/agentContextRecord.d.ts +25 -0
- package/dist/agents/agentContextRecord.d.ts.map +1 -1
- package/dist/agents/agentContextRecord.js +57 -0
- package/dist/agents/agentContextRecord.js.map +1 -1
- package/dist/modules/execution/ExecutionController.js +5 -0
- package/dist/modules/execution/ExecutionController.js.map +1 -1
- package/dist/modules/publicApi/openapiDocument.generated.d.ts +1 -1
- package/dist/modules/publicApi/openapiDocument.generated.js +1 -1
- package/package.json +7 -7
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { BootstrapJobHandle, BootstrapJobRepository, BootstrapJobUpdate, BootstrapRepoOutcome, BootstrapRepoRequest, GitHubClient, MonorepoTargetRepo, GitHubInstallationRepository, GitHubRepo, GroupCacheHandle, ModelRef, RepoBootstrapper, RepoProjectionRepository } from '@cat-factory/kernel';
|
|
1
|
+
import type { AgentContextRecorder, BootstrapJobHandle, BootstrapJobRepository, BootstrapJobUpdate, BootstrapRepoOutcome, BootstrapRepoRequest, GitHubClient, MonorepoTargetRepo, ReferenceRepoAccess, GitHubInstallationRepository, GitHubRepo, GroupCacheHandle, ModelRef, RepoBootstrapper, RepoProjectionRepository, VcsProvider } from '@cat-factory/kernel';
|
|
2
2
|
import type { ContainerSessionService } from '../containers/ContainerSessionService.js';
|
|
3
|
+
import { type ToolTrajectoryDeps } from './toolTrajectory.js';
|
|
3
4
|
import type { JobPackageRegistrySpec } from './ContainerAgentExecutor.js';
|
|
4
|
-
import type
|
|
5
|
+
import { type MintInstallationToken } from './repoTargeting.js';
|
|
5
6
|
import { type ResolveRunnerTransport } from './RunnerJobClient.js';
|
|
6
|
-
export interface ContainerRepoBootstrapperDependencies {
|
|
7
|
+
export interface ContainerRepoBootstrapperDependencies extends ToolTrajectoryDeps {
|
|
7
8
|
/**
|
|
8
9
|
* Resolve which runner backend (Cloudflare container or self-hosted pool) a
|
|
9
10
|
* bootstrap job dispatches to — the same seam the implementation executor rides.
|
|
@@ -23,6 +24,18 @@ export interface ContainerRepoBootstrapperDependencies {
|
|
|
23
24
|
repoProjectionCache?: GroupCacheHandle<GitHubRepo[]>;
|
|
24
25
|
/** Resolves/validates the pre-created target repository (existence + emptiness). */
|
|
25
26
|
githubClient: GitHubClient;
|
|
27
|
+
/**
|
|
28
|
+
* The provider {@link githubClient} speaks (`engineVcsProvider`), so a workspace whose
|
|
29
|
+
* connection is on ANOTHER one is refused rather than probed with the wrong client.
|
|
30
|
+
*
|
|
31
|
+
* Required rather than defaulted, for the reason `makeResolveRepoFilesForCoords` states it:
|
|
32
|
+
* a deployment serving a GitHub App beside per-workspace GitLab connections binds the App
|
|
33
|
+
* client here, and a GitLab workspace's installation id means nothing to it. Reading the
|
|
34
|
+
* template through it anyway answers 404, which this component would then report as "your
|
|
35
|
+
* reference architecture names the wrong repository" for an entry that is perfectly correct.
|
|
36
|
+
* A facade that forgets to state it must fail to typecheck rather than inherit a guess.
|
|
37
|
+
*/
|
|
38
|
+
clientProvider: VcsProvider;
|
|
26
39
|
/**
|
|
27
40
|
* Mints a short-lived GitHub installation token for clone + push, scoped to the single repo
|
|
28
41
|
* being bootstrapped. Bootstrap has no run initiator, so it names no `initiatedBy` and always
|
|
@@ -39,6 +52,12 @@ export interface ContainerRepoBootstrapperDependencies {
|
|
|
39
52
|
githubApiBase?: string;
|
|
40
53
|
/** Web base for building the created repo's URL (defaults to github.com). */
|
|
41
54
|
webBaseUrl?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Records the complete context each bootstrap dispatch handed its agent (best-effort, gated
|
|
57
|
+
* inside the recorder). Absent ⇒ a bootstrap's "Provided context" tab is empty, which is the
|
|
58
|
+
* shape every other agent run's would have if its executor skipped this call.
|
|
59
|
+
*/
|
|
60
|
+
agentContextObservability?: AgentContextRecorder;
|
|
42
61
|
/**
|
|
43
62
|
* Resolve the workspace's private package-registry entries for the bootstrap
|
|
44
63
|
* container (the scaffolder installs dependencies too). Same seam as
|
|
@@ -69,6 +88,43 @@ export declare class ContainerRepoBootstrapper implements RepoBootstrapper {
|
|
|
69
88
|
constructor(deps: ContainerRepoBootstrapperDependencies);
|
|
70
89
|
/** An active (non-soft-deleted) installation means the workspace is connected. */
|
|
71
90
|
isWorkspaceConnected(workspaceId: string): Promise<boolean>;
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the reference template through the workspace's INSTALLATION, which is the same reach
|
|
93
|
+
* the run's clone has.
|
|
94
|
+
*
|
|
95
|
+
* Deliberately NOT the workspace's repo projection (`resolveRepoFilesForCoords`), the way every
|
|
96
|
+
* other checkout-free read here is scoped. A reference architecture is an admin-managed entry
|
|
97
|
+
* naming `owner/name`, not a repository the board has linked, so the projection answers "no such
|
|
98
|
+
* repo" for a template the run then clones without trouble: the survey used to report the
|
|
99
|
+
* template as unsurveyed on every deployment whose template is not also a board service. What
|
|
100
|
+
* scopes this instead is the entry itself plus the installation's own grant, which is the pair
|
|
101
|
+
* that decides whether the clone works.
|
|
102
|
+
*
|
|
103
|
+
* A 404 is the provider's answer that this credential cannot see the repository, which on GitHub
|
|
104
|
+
* covers both "no such repo" and "the App was not granted it", and it is one verdict because
|
|
105
|
+
* they take the same fix. Any other failure is reported as UNREADABLE rather than as an absence,
|
|
106
|
+
* so an outage cannot be presented to an operator as a typo in their configuration.
|
|
107
|
+
*
|
|
108
|
+
* NEVER throws, which is a contract two callers depend on (the survey, whose whole phase is
|
|
109
|
+
* "park with what we know", and the pre-flight, which owes a 503). So the installation read sits
|
|
110
|
+
* INSIDE the try as well: on a mothership-mode node the repository read is an RPC to a process
|
|
111
|
+
* that can be a restart away, and a throw from there would arrive as a platform bug about
|
|
112
|
+
* somebody's own template.
|
|
113
|
+
*/
|
|
114
|
+
resolveReferenceRepo(workspaceId: string, ref: {
|
|
115
|
+
owner: string;
|
|
116
|
+
name: string;
|
|
117
|
+
}): Promise<ReferenceRepoAccess>;
|
|
118
|
+
/**
|
|
119
|
+
* The one provider read behind every reference-template question: the pre-flight's verdict, the
|
|
120
|
+
* survey's reader and the dispatch's clone spec are all this, mapped three ways.
|
|
121
|
+
*
|
|
122
|
+
* Shared because the two callers used to ask it differently: one 404-aware and returning a
|
|
123
|
+
* verdict, the other collapsing every failure into one disposition. So the same rate limit was
|
|
124
|
+
* reported as the entry being wrong at one moment and as an outage at another. One probe, one
|
|
125
|
+
* set of causes, and each caller decides only what to DO about them.
|
|
126
|
+
*/
|
|
127
|
+
private readTemplate;
|
|
72
128
|
/**
|
|
73
129
|
* Resolve a monorepo bootstrap target out of the WORKSPACE's own repo projection.
|
|
74
130
|
*
|
|
@@ -153,23 +209,80 @@ export declare class ContainerRepoBootstrapper implements RepoBootstrapper {
|
|
|
153
209
|
* Resolve the reference template a run reads from: its clone spec, plus the numeric id the
|
|
154
210
|
* run's installation token has to be scoped to.
|
|
155
211
|
*
|
|
156
|
-
* ONE resolution for
|
|
212
|
+
* ONE resolution for every dispatch shape, because they all CLONE the template and all mint the
|
|
157
213
|
* token that clone runs on. Two copies is what left the force-push path granting only its push
|
|
158
214
|
* target while cloning the template with that same token, so a PRIVATE reference architecture
|
|
159
215
|
* 404s on clone under one delivery and works under the other, and what left it assuming the
|
|
160
216
|
* template's base branch was `main`.
|
|
161
217
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* is
|
|
218
|
+
* It THROWS on a template it cannot resolve, rather than dispatching with no id and the
|
|
219
|
+
* conventional branch: the container is about to clone it, so the run is over either way, and
|
|
220
|
+
* the difference is whether the report names the reference architecture and what to do about it
|
|
221
|
+
* or arrives as a git error from inside a container that had to be started first.
|
|
222
|
+
*
|
|
223
|
+
* The MESSAGE follows the cause the shared probe reported. "Point the reference architecture at
|
|
224
|
+
* a repository this workspace can reach" is the wrong instruction for a rate limit or a 500,
|
|
225
|
+
* and a monorepo apply dispatches days after a human settled its review, so the failure read
|
|
226
|
+
* here is as likely to be an outage as a typo. The provider error rides as `cause`, so a log
|
|
227
|
+
* describer walking the chain can still see which it was.
|
|
228
|
+
*
|
|
229
|
+
* Re-read rather than carried over from the run-start pre-flight, deliberately: this is the
|
|
230
|
+
* moment the clone happens, and on a monorepo run the pre-flight sits on the other side of a
|
|
231
|
+
* human review. An id plumbed through the request would be a claim about the repository as it
|
|
232
|
+
* was, dispatched against the repository as it is.
|
|
165
233
|
*/
|
|
166
234
|
private resolveReferenceTemplate;
|
|
167
|
-
/**
|
|
235
|
+
/**
|
|
236
|
+
* Mint the job's clone/push token, scoped to the repos this dispatch resolved.
|
|
237
|
+
*
|
|
238
|
+
* The scope goes through the SHARED `jobTokenRepoIds` rather than a hand-built array, for the
|
|
239
|
+
* case the hand-built one got wrong: a reference architecture naming the run's own target (or
|
|
240
|
+
* its monorepo) is one repository asked for twice, and a `repository_ids` list is a set.
|
|
241
|
+
*
|
|
242
|
+
* A refusal is re-thrown NAMING the template, because a scoped mint has one failure mode this
|
|
243
|
+
* component cannot pre-flight away. GitHub narrows a token only to repositories the
|
|
244
|
+
* installation was GRANTED, while a PUBLIC repository reads perfectly well through the API
|
|
245
|
+
* without being one, so a template outside the App's repository access resolves cleanly above
|
|
246
|
+
* and is refused here. There is no read on this port that separates the two cheaply and
|
|
247
|
+
* correctly on every provider (`getRepoById` answers from a bounded listing on the GitLab
|
|
248
|
+
* adapter, so a null there is not evidence of anything), and dropping the leg would only move
|
|
249
|
+
* the failure to the clone. What is left is to make the refusal say which repository to grant.
|
|
250
|
+
*/
|
|
251
|
+
private mintDispatchToken;
|
|
252
|
+
/**
|
|
253
|
+
* The model-locked LLM-proxy session token a bootstrap container runs under.
|
|
254
|
+
*
|
|
255
|
+
* Keyed on the RUN, never on the drive: a monorepo run's apply phase is dispatched under its
|
|
256
|
+
* own container job id, so minting on that id files the apply's model calls under a key no
|
|
257
|
+
* run-scoped read asks for, and the run reports only what its survey spent.
|
|
258
|
+
*/
|
|
168
259
|
private mintSessionToken;
|
|
169
260
|
/** The host's web base, trailing slashes stripped, for building clone URLs. */
|
|
170
261
|
private webBase;
|
|
171
262
|
/** Poll a dispatched bootstrap job, mapping the runner job view into an update. */
|
|
172
263
|
pollBootstrap(handle: BootstrapJobHandle): Promise<BootstrapJobUpdate>;
|
|
264
|
+
/**
|
|
265
|
+
* The dispatched container job as an {@link AgentJobHandle}, the shape the shared trajectory
|
|
266
|
+
* drain speaks. `runId` is the bootstrap RUN and `jobId` the container job it dispatched,
|
|
267
|
+
* which is the same split every execution step's handle carries.
|
|
268
|
+
*/
|
|
269
|
+
private jobHandle;
|
|
270
|
+
/**
|
|
271
|
+
* The resolved model as every OTHER producer writes it: `provider:model`, which is the format
|
|
272
|
+
* `AgentJobHandle.model` and `agentContextSnapshotSchema.model` both document. A bare model id
|
|
273
|
+
* renders beside prefixed ones on the same panel and gives nothing to a reader that splits the
|
|
274
|
+
* field to recover the provider.
|
|
275
|
+
*/
|
|
276
|
+
private resolvedModel;
|
|
277
|
+
/**
|
|
278
|
+
* File what this dispatch handed the agent, so a bootstrap's Provided-context tab answers the
|
|
279
|
+
* same question every other agent run's does.
|
|
280
|
+
*
|
|
281
|
+
* AWAITED for the reason `recordAgentContextSnapshot` states: it runs after the container has
|
|
282
|
+
* already been accepted, so it delays nothing but the handle's return, and an un-awaited insert
|
|
283
|
+
* is dropped outright on the Worker, where this runs inside a Workflow step.
|
|
284
|
+
*/
|
|
285
|
+
private recordDispatchContext;
|
|
173
286
|
/**
|
|
174
287
|
* Best-effort: reclaim the per-run container for a job. Releases through the same
|
|
175
288
|
* transport the run dispatched to (keyed by job id) — for the Cloudflare backend
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContainerRepoBootstrapper.d.ts","sourceRoot":"","sources":["../../src/agents/ContainerRepoBootstrapper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"ContainerRepoBootstrapper.d.ts","sourceRoot":"","sources":["../../src/agents/ContainerRepoBootstrapper.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { failureKindFromHarnessCause, runBestEffort } from '@cat-factory/kernel';
|
|
1
|
+
import { failureKindFromHarnessCause, getErrorMessage, runBestEffort, VcsApiError, } from '@cat-factory/kernel';
|
|
2
2
|
import { isProxyableProvider } from '@cat-factory/agents';
|
|
3
|
+
import { bootstrapStepIds, REPO_BOOTSTRAP_AGENT_KIND } from '@cat-factory/contracts';
|
|
4
|
+
import { recordBootstrapContextSnapshot } from './agentContextRecord.js';
|
|
5
|
+
import { drainToolCalls } from './toolTrajectory.js';
|
|
6
|
+
import { makeRepoFiles } from './repoFiles.js';
|
|
7
|
+
import { jobTokenRepoIds } from './repoTargeting.js';
|
|
3
8
|
import { RunnerJobClient } from './RunnerJobClient.js';
|
|
4
9
|
import { logger } from '../observability/logger.js';
|
|
5
10
|
/** The role prompt when adapting a cloned reference architecture. */
|
|
@@ -127,6 +132,87 @@ export class ContainerRepoBootstrapper {
|
|
|
127
132
|
const installation = await this.deps.installationRepository.getByWorkspace(workspaceId);
|
|
128
133
|
return !!installation && !installation.deletedAt;
|
|
129
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Resolve the reference template through the workspace's INSTALLATION, which is the same reach
|
|
137
|
+
* the run's clone has.
|
|
138
|
+
*
|
|
139
|
+
* Deliberately NOT the workspace's repo projection (`resolveRepoFilesForCoords`), the way every
|
|
140
|
+
* other checkout-free read here is scoped. A reference architecture is an admin-managed entry
|
|
141
|
+
* naming `owner/name`, not a repository the board has linked, so the projection answers "no such
|
|
142
|
+
* repo" for a template the run then clones without trouble: the survey used to report the
|
|
143
|
+
* template as unsurveyed on every deployment whose template is not also a board service. What
|
|
144
|
+
* scopes this instead is the entry itself plus the installation's own grant, which is the pair
|
|
145
|
+
* that decides whether the clone works.
|
|
146
|
+
*
|
|
147
|
+
* A 404 is the provider's answer that this credential cannot see the repository, which on GitHub
|
|
148
|
+
* covers both "no such repo" and "the App was not granted it", and it is one verdict because
|
|
149
|
+
* they take the same fix. Any other failure is reported as UNREADABLE rather than as an absence,
|
|
150
|
+
* so an outage cannot be presented to an operator as a typo in their configuration.
|
|
151
|
+
*
|
|
152
|
+
* NEVER throws, which is a contract two callers depend on (the survey, whose whole phase is
|
|
153
|
+
* "park with what we know", and the pre-flight, which owes a 503). So the installation read sits
|
|
154
|
+
* INSIDE the try as well: on a mothership-mode node the repository read is an RPC to a process
|
|
155
|
+
* that can be a restart away, and a throw from there would arrive as a platform bug about
|
|
156
|
+
* somebody's own template.
|
|
157
|
+
*/
|
|
158
|
+
async resolveReferenceRepo(workspaceId, ref) {
|
|
159
|
+
let installation;
|
|
160
|
+
try {
|
|
161
|
+
installation = await this.deps.installationRepository.getByWorkspace(workspaceId);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
// A connection we cannot READ is not a connection we know to be absent: `not_connected`
|
|
165
|
+
// would tell the operator to install an App that may well already be installed.
|
|
166
|
+
return { status: 'unreadable', detail: getErrorMessage(error) };
|
|
167
|
+
}
|
|
168
|
+
if (!installation || installation.deletedAt)
|
|
169
|
+
return { status: 'not_connected' };
|
|
170
|
+
// The row's own provider, falling back to this client's for a row predating the column. A
|
|
171
|
+
// connection on another provider is not reachable from here whatever it holds, and reporting
|
|
172
|
+
// that as `not_found` is the misattribution the verdict split exists to prevent.
|
|
173
|
+
if ((installation.provider ?? this.deps.clientProvider) !== this.deps.clientProvider) {
|
|
174
|
+
return { status: 'not_connected' };
|
|
175
|
+
}
|
|
176
|
+
const read = await this.readTemplate(installation.installationId, ref);
|
|
177
|
+
if (read.status === 'not_found')
|
|
178
|
+
return { status: 'not_found' };
|
|
179
|
+
// Mapped field by field rather than passed through: the verdict crosses a port and becomes an
|
|
180
|
+
// HTTP `details.detail`, and the thrown value the probe kept for its own `cause` chain has no
|
|
181
|
+
// business on a wire shape.
|
|
182
|
+
if (read.status === 'unreadable')
|
|
183
|
+
return { status: 'unreadable', detail: read.detail };
|
|
184
|
+
return {
|
|
185
|
+
status: 'reachable',
|
|
186
|
+
files: makeRepoFiles(this.deps.githubClient, installation.installationId, {
|
|
187
|
+
owner: ref.owner,
|
|
188
|
+
repo: ref.name,
|
|
189
|
+
}),
|
|
190
|
+
defaultBranch: defaultBranchOf(read.repo),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The one provider read behind every reference-template question: the pre-flight's verdict, the
|
|
195
|
+
* survey's reader and the dispatch's clone spec are all this, mapped three ways.
|
|
196
|
+
*
|
|
197
|
+
* Shared because the two callers used to ask it differently: one 404-aware and returning a
|
|
198
|
+
* verdict, the other collapsing every failure into one disposition. So the same rate limit was
|
|
199
|
+
* reported as the entry being wrong at one moment and as an outage at another. One probe, one
|
|
200
|
+
* set of causes, and each caller decides only what to DO about them.
|
|
201
|
+
*/
|
|
202
|
+
async readTemplate(installationId, ref) {
|
|
203
|
+
try {
|
|
204
|
+
const repo = await this.deps.githubClient.getRepo(installationId, {
|
|
205
|
+
owner: ref.owner,
|
|
206
|
+
repo: ref.name,
|
|
207
|
+
});
|
|
208
|
+
return { status: 'reachable', repo };
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (error instanceof VcsApiError && error.status === 404)
|
|
212
|
+
return { status: 'not_found' };
|
|
213
|
+
return { status: 'unreadable', detail: getErrorMessage(error), cause: error };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
130
216
|
/**
|
|
131
217
|
* Resolve a monorepo bootstrap target out of the WORKSPACE's own repo projection.
|
|
132
218
|
*
|
|
@@ -190,23 +276,19 @@ export class ContainerRepoBootstrapper {
|
|
|
190
276
|
// With a reference architecture the container clones + adapts it; without one
|
|
191
277
|
// it scaffolds an empty repo from the freeform instructions alone.
|
|
192
278
|
const reference = await this.resolveReferenceTemplate(request, installation.installationId, log);
|
|
279
|
+
const webBase = this.webBase();
|
|
280
|
+
const targetCloneUrl = `${webBase}/${owner}/${repoName}.git`;
|
|
281
|
+
const defaultBranch = defaultBranchOf(target);
|
|
193
282
|
// Scoped to the repos this run touches: the target it force-pushes, and the template it
|
|
194
283
|
// CLONES. The template belongs on the scope even though nothing pushes to it, because the
|
|
195
284
|
// clone runs on this same token: leaving it off works only for a public reference
|
|
196
285
|
// architecture and 404s on a private one, with nothing in the failure naming the cause.
|
|
197
286
|
// `target` came from the pre-flight `getRepo` above, which is why it costs no extra read.
|
|
198
|
-
const ghToken = await this.
|
|
199
|
-
executionId: request.containerJobId,
|
|
200
|
-
workspaceId: request.workspaceId,
|
|
201
|
-
repoIds: [String(target.githubId), ...(reference?.githubId ? [reference.githubId] : [])],
|
|
202
|
-
});
|
|
287
|
+
const ghToken = await this.mintDispatchToken(request, installation.installationId, { owner, name: repoName, repoId: String(target.githubId), baseBranch: defaultBranch }, reference);
|
|
203
288
|
// Private-registry auth for the scaffolder's installs, exactly as the
|
|
204
289
|
// implementation executor forwards it.
|
|
205
290
|
const packageRegistries = (await this.deps.resolvePackageRegistries?.(request.workspaceId)) ?? [];
|
|
206
291
|
const sessionToken = await this.mintSessionToken(request);
|
|
207
|
-
const webBase = this.webBase();
|
|
208
|
-
const targetCloneUrl = `${webBase}/${owner}/${repoName}.git`;
|
|
209
|
-
const defaultBranch = defaultBranchOf(target);
|
|
210
292
|
const targetSpec = { owner, name: repoName, cloneUrl: targetCloneUrl, defaultBranch };
|
|
211
293
|
// The generic agent `repo` is the clone source: the reference when adapting one, or the
|
|
212
294
|
// (uncloned) target placeholder when scaffolding from scratch. The real push destination
|
|
@@ -226,12 +308,12 @@ export class ContainerRepoBootstrapper {
|
|
|
226
308
|
jobId: request.containerJobId,
|
|
227
309
|
// The run's correlation ids, so the container's own lines join to this bootstrap in the
|
|
228
310
|
// backend's logs: the same fields `buildCommonBody` puts on an execution job. A bootstrap
|
|
229
|
-
// is a first-class agent run (one `agent_runs` table, one retry surface
|
|
230
|
-
// the one agent-kind dispatch whose container logs cannot be
|
|
231
|
-
//
|
|
232
|
-
//
|
|
311
|
+
// is a first-class agent run (one `agent_runs` table, one retry surface, one observability
|
|
312
|
+
// panel), so it must not be the one agent-kind dispatch whose container logs cannot be
|
|
313
|
+
// joined to anything. The id is the RUN's, matching the session token above: a bootstrap
|
|
314
|
+
// has no separate execution row, so its run id is what every run-scoped read is keyed by.
|
|
233
315
|
workspaceId: request.workspaceId,
|
|
234
|
-
executionId: request.
|
|
316
|
+
executionId: request.jobId,
|
|
235
317
|
mode: 'coding',
|
|
236
318
|
systemPrompt: reference ? ADAPT_SYSTEM_PROMPT : SCAFFOLD_SYSTEM_PROMPT,
|
|
237
319
|
userPrompt: request.instructions ||
|
|
@@ -273,6 +355,7 @@ export class ContainerRepoBootstrapper {
|
|
|
273
355
|
// equals the run id (no per-step fan-out into a shared container, and no second phase).
|
|
274
356
|
await this.jobs.dispatch(request.workspaceId, { runId: request.jobId, jobId: request.containerJobId }, body, 'agent');
|
|
275
357
|
log.info('bootstrap: container accepted job');
|
|
358
|
+
await this.recordDispatchContext(request, body, log);
|
|
276
359
|
return {
|
|
277
360
|
workspaceId: request.workspaceId,
|
|
278
361
|
jobId: request.jobId,
|
|
@@ -443,14 +526,10 @@ export class ContainerRepoBootstrapper {
|
|
|
443
526
|
* carrying a branch but no PR pushes work onto a branch nobody is told about.
|
|
444
527
|
*/
|
|
445
528
|
async dispatchCodingShape(request, installation, log, spec) {
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
//
|
|
529
|
+
// Re-flighted here for the same reason the monorepo's target directory is: an apply dispatch
|
|
530
|
+
// can be days after the review, and a template that has since moved out of reach is a clone
|
|
531
|
+
// this run is about to fail.
|
|
449
532
|
const reference = await this.resolveReferenceTemplate(request, installation.installationId, log);
|
|
450
|
-
const repoIds = [
|
|
451
|
-
String(spec.repoGithubId),
|
|
452
|
-
...(reference?.githubId ? [reference.githubId] : []),
|
|
453
|
-
];
|
|
454
533
|
const referenceRepos = reference
|
|
455
534
|
? [
|
|
456
535
|
{
|
|
@@ -463,11 +542,13 @@ export class ContainerRepoBootstrapper {
|
|
|
463
542
|
},
|
|
464
543
|
]
|
|
465
544
|
: [];
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
545
|
+
// Scoped to the repos this run touches: the one it pushes to, and the template it reads.
|
|
546
|
+
const ghToken = await this.mintDispatchToken(request, installation.installationId, {
|
|
547
|
+
owner: spec.repo.owner,
|
|
548
|
+
name: spec.repo.name,
|
|
549
|
+
repoId: String(spec.repoGithubId),
|
|
550
|
+
baseBranch: spec.repo.baseBranch,
|
|
551
|
+
}, reference);
|
|
471
552
|
const packageRegistries = (await this.deps.resolvePackageRegistries?.(request.workspaceId)) ?? [];
|
|
472
553
|
const sessionToken = await this.mintSessionToken(request);
|
|
473
554
|
// The delivery decides the two fields TOGETHER: with `newBranch` and `pr` the harness pushes
|
|
@@ -479,7 +560,8 @@ export class ContainerRepoBootstrapper {
|
|
|
479
560
|
const body = {
|
|
480
561
|
jobId: request.containerJobId,
|
|
481
562
|
workspaceId: request.workspaceId,
|
|
482
|
-
|
|
563
|
+
// The RUN, not this phase's drive id: see the session mint above.
|
|
564
|
+
executionId: request.jobId,
|
|
483
565
|
mode: 'coding',
|
|
484
566
|
systemPrompt: spec.systemPrompt,
|
|
485
567
|
userPrompt: spec.userPrompt,
|
|
@@ -504,6 +586,7 @@ export class ContainerRepoBootstrapper {
|
|
|
504
586
|
branch: request.delivery.mode === 'pull_request' ? request.delivery.branch : spec.repo.baseBranch,
|
|
505
587
|
reference: reference ? `${reference.owner}/${reference.name}` : null,
|
|
506
588
|
});
|
|
589
|
+
await this.recordDispatchContext(request, body, log);
|
|
507
590
|
return {
|
|
508
591
|
workspaceId: request.workspaceId,
|
|
509
592
|
jobId: request.jobId,
|
|
@@ -514,38 +597,106 @@ export class ContainerRepoBootstrapper {
|
|
|
514
597
|
* Resolve the reference template a run reads from: its clone spec, plus the numeric id the
|
|
515
598
|
* run's installation token has to be scoped to.
|
|
516
599
|
*
|
|
517
|
-
* ONE resolution for
|
|
600
|
+
* ONE resolution for every dispatch shape, because they all CLONE the template and all mint the
|
|
518
601
|
* token that clone runs on. Two copies is what left the force-push path granting only its push
|
|
519
602
|
* target while cloning the template with that same token, so a PRIVATE reference architecture
|
|
520
603
|
* 404s on clone under one delivery and works under the other, and what left it assuming the
|
|
521
604
|
* template's base branch was `main`.
|
|
522
605
|
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
* is
|
|
606
|
+
* It THROWS on a template it cannot resolve, rather than dispatching with no id and the
|
|
607
|
+
* conventional branch: the container is about to clone it, so the run is over either way, and
|
|
608
|
+
* the difference is whether the report names the reference architecture and what to do about it
|
|
609
|
+
* or arrives as a git error from inside a container that had to be started first.
|
|
610
|
+
*
|
|
611
|
+
* The MESSAGE follows the cause the shared probe reported. "Point the reference architecture at
|
|
612
|
+
* a repository this workspace can reach" is the wrong instruction for a rate limit or a 500,
|
|
613
|
+
* and a monorepo apply dispatches days after a human settled its review, so the failure read
|
|
614
|
+
* here is as likely to be an outage as a typo. The provider error rides as `cause`, so a log
|
|
615
|
+
* describer walking the chain can still see which it was.
|
|
616
|
+
*
|
|
617
|
+
* Re-read rather than carried over from the run-start pre-flight, deliberately: this is the
|
|
618
|
+
* moment the clone happens, and on a monorepo run the pre-flight sits on the other side of a
|
|
619
|
+
* human review. An id plumbed through the request would be a claim about the repository as it
|
|
620
|
+
* was, dispatched against the repository as it is.
|
|
526
621
|
*/
|
|
527
622
|
async resolveReferenceTemplate(request, installationId, log) {
|
|
528
623
|
const reference = request.referenceRepo;
|
|
529
624
|
if (!reference)
|
|
530
625
|
return undefined;
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
626
|
+
const repo = `${reference.owner}/${reference.name}`;
|
|
627
|
+
const read = await this.readTemplate(installationId, reference);
|
|
628
|
+
if (read.status === 'reachable') {
|
|
629
|
+
return {
|
|
630
|
+
owner: reference.owner,
|
|
631
|
+
name: reference.name,
|
|
632
|
+
repoId: String(read.repo.githubId),
|
|
633
|
+
baseBranch: defaultBranchOf(read.repo),
|
|
634
|
+
cloneUrl: `${this.webBase()}/${reference.owner}/${reference.name}.git`,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
log.warn('bootstrap: reference template could not be resolved', {
|
|
638
|
+
reference: repo,
|
|
639
|
+
verdict: read.status,
|
|
640
|
+
});
|
|
641
|
+
if (read.status === 'not_found') {
|
|
642
|
+
throw new Error(`The reference template ${repo} cannot be seen through this workspace's source-control ` +
|
|
643
|
+
`connection, so it cannot be cloned. Point the reference architecture at a repository ` +
|
|
644
|
+
`this workspace can reach, or grant the App access to it, then retry.`);
|
|
645
|
+
}
|
|
646
|
+
throw new Error(`The reference template ${repo} could not be read just now, so this run was not dispatched ` +
|
|
647
|
+
`rather than dispatched against a template it may be unable to clone. Nothing here is ` +
|
|
648
|
+
`misconfigured: retry once the source-control connection recovers. Cause: ${read.detail}`, { cause: read.cause });
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Mint the job's clone/push token, scoped to the repos this dispatch resolved.
|
|
652
|
+
*
|
|
653
|
+
* The scope goes through the SHARED `jobTokenRepoIds` rather than a hand-built array, for the
|
|
654
|
+
* case the hand-built one got wrong: a reference architecture naming the run's own target (or
|
|
655
|
+
* its monorepo) is one repository asked for twice, and a `repository_ids` list is a set.
|
|
656
|
+
*
|
|
657
|
+
* A refusal is re-thrown NAMING the template, because a scoped mint has one failure mode this
|
|
658
|
+
* component cannot pre-flight away. GitHub narrows a token only to repositories the
|
|
659
|
+
* installation was GRANTED, while a PUBLIC repository reads perfectly well through the API
|
|
660
|
+
* without being one, so a template outside the App's repository access resolves cleanly above
|
|
661
|
+
* and is refused here. There is no read on this port that separates the two cheaply and
|
|
662
|
+
* correctly on every provider (`getRepoById` answers from a bounded listing on the GitLab
|
|
663
|
+
* adapter, so a null there is not evidence of anything), and dropping the leg would only move
|
|
664
|
+
* the failure to the clone. What is left is to make the refusal say which repository to grant.
|
|
665
|
+
*/
|
|
666
|
+
async mintDispatchToken(request, installationId, primary, template) {
|
|
667
|
+
const asTarget = (leg) => ({ installationId, ...leg });
|
|
668
|
+
try {
|
|
669
|
+
return await this.deps.mintInstallationToken(installationId, {
|
|
670
|
+
// The RUN, not this phase's drive id: see the session mint below.
|
|
671
|
+
executionId: request.jobId,
|
|
672
|
+
workspaceId: request.workspaceId,
|
|
673
|
+
repoIds: jobTokenRepoIds(asTarget(primary), template ? [asTarget(template)] : []),
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
catch (error) {
|
|
677
|
+
if (!template)
|
|
678
|
+
throw error;
|
|
679
|
+
throw new Error(`The source-control token for this run could not be issued for both ` +
|
|
680
|
+
`${primary.owner}/${primary.name} and the reference template ` +
|
|
681
|
+
`${template.owner}/${template.name}. A GitHub App token can only cover repositories ` +
|
|
682
|
+
`the installation has been GRANTED, and a public repository reads through the API ` +
|
|
683
|
+
`without being one. Grant the App access to ${template.owner}/${template.name}, or ` +
|
|
684
|
+
`point the reference architecture at a repository it already covers, then retry. ` +
|
|
685
|
+
`Cause: ${getErrorMessage(error)}`, { cause: error });
|
|
686
|
+
}
|
|
542
687
|
}
|
|
543
|
-
/**
|
|
688
|
+
/**
|
|
689
|
+
* The model-locked LLM-proxy session token a bootstrap container runs under.
|
|
690
|
+
*
|
|
691
|
+
* Keyed on the RUN, never on the drive: a monorepo run's apply phase is dispatched under its
|
|
692
|
+
* own container job id, so minting on that id files the apply's model calls under a key no
|
|
693
|
+
* run-scoped read asks for, and the run reports only what its survey spent.
|
|
694
|
+
*/
|
|
544
695
|
mintSessionToken(request) {
|
|
545
696
|
return this.deps.sessionService.mint({
|
|
546
697
|
workspaceId: request.workspaceId,
|
|
547
|
-
executionId: request.
|
|
548
|
-
agentKind:
|
|
698
|
+
executionId: request.jobId,
|
|
699
|
+
agentKind: REPO_BOOTSTRAP_AGENT_KIND,
|
|
549
700
|
provider: this.deps.model.provider,
|
|
550
701
|
model: this.deps.model.model,
|
|
551
702
|
});
|
|
@@ -560,6 +711,13 @@ export class ContainerRepoBootstrapper {
|
|
|
560
711
|
runId: handle.jobId,
|
|
561
712
|
jobId: handle.containerJobId,
|
|
562
713
|
});
|
|
714
|
+
// The tool calls the harness drained on this poll, to the same two destinations an
|
|
715
|
+
// execution step's go to. Filed under the RUN (`runId`) and grouped by the container job,
|
|
716
|
+
// so a monorepo run's apply trajectory reads under the run a person opened. Isolated +
|
|
717
|
+
// best-effort inside `drainToolCalls`: it can never affect this poll's verdict.
|
|
718
|
+
// Correlated by the same two ids every other line about this run carries, so a drain that
|
|
719
|
+
// warns (an image too old to number its calls) names the run it was about.
|
|
720
|
+
await drainToolCalls(this.deps, this.jobHandle(handle), view.spans, logger.child({ jobId: handle.jobId, workspaceId: handle.workspaceId }));
|
|
563
721
|
if (view.state === 'running') {
|
|
564
722
|
return view.progress ? { state: 'running', subtasks: view.progress } : { state: 'running' };
|
|
565
723
|
}
|
|
@@ -608,6 +766,48 @@ export class ContainerRepoBootstrapper {
|
|
|
608
766
|
const outcome = await this.buildOutcome(handle, record.repoName, result.defaultBranch);
|
|
609
767
|
return { state: 'done', outcome, ...pr };
|
|
610
768
|
}
|
|
769
|
+
/**
|
|
770
|
+
* The dispatched container job as an {@link AgentJobHandle}, the shape the shared trajectory
|
|
771
|
+
* drain speaks. `runId` is the bootstrap RUN and `jobId` the container job it dispatched,
|
|
772
|
+
* which is the same split every execution step's handle carries.
|
|
773
|
+
*/
|
|
774
|
+
jobHandle(handle) {
|
|
775
|
+
return {
|
|
776
|
+
jobId: handle.containerJobId,
|
|
777
|
+
runId: handle.jobId,
|
|
778
|
+
workspaceId: handle.workspaceId,
|
|
779
|
+
agentKind: REPO_BOOTSTRAP_AGENT_KIND,
|
|
780
|
+
model: this.resolvedModel(),
|
|
781
|
+
provider: this.deps.model.provider,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* The resolved model as every OTHER producer writes it: `provider:model`, which is the format
|
|
786
|
+
* `AgentJobHandle.model` and `agentContextSnapshotSchema.model` both document. A bare model id
|
|
787
|
+
* renders beside prefixed ones on the same panel and gives nothing to a reader that splits the
|
|
788
|
+
* field to recover the provider.
|
|
789
|
+
*/
|
|
790
|
+
resolvedModel() {
|
|
791
|
+
return `${this.deps.model.provider}:${this.deps.model.model}`;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* File what this dispatch handed the agent, so a bootstrap's Provided-context tab answers the
|
|
795
|
+
* same question every other agent run's does.
|
|
796
|
+
*
|
|
797
|
+
* AWAITED for the reason `recordAgentContextSnapshot` states: it runs after the container has
|
|
798
|
+
* already been accepted, so it delays nothing but the handle's return, and an un-awaited insert
|
|
799
|
+
* is dropped outright on the Worker, where this runs inside a Workflow step.
|
|
800
|
+
*/
|
|
801
|
+
async recordDispatchContext(request, body, log) {
|
|
802
|
+
await recordBootstrapContextSnapshot(this.deps.agentContextObservability, log, {
|
|
803
|
+
body,
|
|
804
|
+
model: this.resolvedModel(),
|
|
805
|
+
agentKind: REPO_BOOTSTRAP_AGENT_KIND,
|
|
806
|
+
workspaceId: request.workspaceId,
|
|
807
|
+
executionId: request.jobId,
|
|
808
|
+
stepIndex: dispatchStepIndex(request),
|
|
809
|
+
});
|
|
810
|
+
}
|
|
611
811
|
/**
|
|
612
812
|
* Best-effort: reclaim the per-run container for a job. Releases through the same
|
|
613
813
|
* transport the run dispatched to (keyed by job id) — for the Cloudflare backend
|
|
@@ -690,6 +890,18 @@ export class ContainerRepoBootstrapper {
|
|
|
690
890
|
return record;
|
|
691
891
|
}
|
|
692
892
|
}
|
|
893
|
+
/**
|
|
894
|
+
* Which of the RUN's own steps this dispatch is, numbered exactly as the board numbers them.
|
|
895
|
+
*
|
|
896
|
+
* The container dispatch is always the run's LAST move: a new-repo run is only `scaffold`, and a
|
|
897
|
+
* monorepo run's apply follows the survey and the human review. So it is read off the end of the
|
|
898
|
+
* shared `bootstrapStepIds` list rather than by searching it for a step NAME, which cannot
|
|
899
|
+
* answer `-1`. A snapshot filed at a step the run never had is a row every step-scoped read
|
|
900
|
+
* silently drops.
|
|
901
|
+
*/
|
|
902
|
+
function dispatchStepIndex(request) {
|
|
903
|
+
return bootstrapStepIds({ monorepo: request.monorepo ?? null }).length - 1;
|
|
904
|
+
}
|
|
693
905
|
/** A repo's default branch, or the conventional fallback when the provider reported none. */
|
|
694
906
|
function defaultBranchOf(repo) {
|
|
695
907
|
return repo.defaultBranch ?? 'main';
|