@bermudi/pi-delegate 0.1.10 → 0.1.12
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 +57 -4
- package/concurrency.ts +55 -16
- package/config.ts +584 -50
- package/delegate.ts +11 -3
- package/dispatch.ts +436 -65
- package/extension.ts +16 -4
- package/format.ts +52 -6
- package/host-cache.ts +70 -0
- package/host.ts +169 -811
- package/isolated-workspace.ts +857 -0
- package/lifecycle.ts +699 -520
- package/manual.ts +4 -7
- package/package.json +1 -1
- package/pi-package-source.ts +293 -0
- package/pool.ts +23 -1
- package/provider-extensions.ts +537 -0
- package/quiescence.ts +262 -0
- package/render-branches.ts +30 -0
- package/render-result.ts +8 -5
- package/runner.ts +35 -141
- package/schema.ts +133 -202
- package/settings.ts +202 -84
- package/shared-write-safety.ts +273 -0
- package/task-resolution.ts +121 -32
- package/telemetry.ts +135 -68
- package/ticket-format.ts +331 -0
- package/tickets.ts +101 -258
- package/tools.ts +12 -0
- package/trusted-paths.ts +71 -0
- package/types.ts +83 -16
package/manual.ts
CHANGED
|
@@ -110,7 +110,7 @@ export function getSubagentManualMarkdown(
|
|
|
110
110
|
'delegate({ tasks: [{ agent: "default", prompt: "Investigate the auth module" }] })',
|
|
111
111
|
"```",
|
|
112
112
|
"",
|
|
113
|
-
|
|
113
|
+
'Delegate subagents to execute tasks in parallel. Each subagent gets an independent conversation. A batch with multiple write-capable tasks in one shared tree is rejected before dispatch; run them sequentially, use synchronous one-shot `workspace: "isolated"` for Git-backed ordered reconciliation, or use disposable scratch workspaces.',
|
|
114
114
|
"",
|
|
115
115
|
"The three handles have different lifetimes:",
|
|
116
116
|
"",
|
|
@@ -136,7 +136,7 @@ export function getSubagentManualMarkdown(
|
|
|
136
136
|
"",
|
|
137
137
|
...builtinLines,
|
|
138
138
|
"",
|
|
139
|
-
"Fresh built-ins inherit the parent's exact model object and thinking level. A same-named Markdown file can override any built-in (first definition wins); an explicit `model` or `thinking` in that file replaces parent inheritance. Task-level `model`/`thinking`/`tools` always win. For `scout`/`coder`/`reviewer`,
|
|
139
|
+
"Fresh built-ins inherit the parent's exact model object and thinking level. A same-named Markdown file can override any built-in (first definition wins); an explicit `model` or `thinking` in that file replaces parent inheritance. Task-level `model`/`thinking`/`tools` always win. For `scout`/`coder`/`reviewer`, delegate.json overrides (`agentOverrides`, `agentOverridesByParentModel`) win over the Markdown file; `default` ignores overrides and uses only an explicit Markdown `model`/`thinking` when present. A prompt-only Markdown override keeps the built-in's tools and workspace, so `scout` stays read-only and `reviewer` stays scratch unless the file explicitly changes them. Parent extension/MCP tools are not copied. Parent-global `AGENTS.md` instructions are also excluded. Project-local context and skills are rebuilt for the task's `cwd`; per-task fields remain explicit overrides.",
|
|
140
140
|
"",
|
|
141
141
|
"## Available Custom Agents",
|
|
142
142
|
"",
|
|
@@ -234,6 +234,7 @@ export function getSubagentManualMarkdown(
|
|
|
234
234
|
"## Gotchas",
|
|
235
235
|
"",
|
|
236
236
|
"- Dispatch validation is batch-wide and runs before spawning: one invalid task rejects the call without starting its siblings.",
|
|
237
|
+
'- Shared writers that overlap tasks in the same call or a running sync/async dispatch are rejected. Unknown tools count as mutating. Run them sequentially, use `workspace: "isolated"` for Git-backed ordered reconciliation, or use `workspace: "scratch"` when changes may be discarded.',
|
|
237
238
|
"- `*` means read/write/edit/bash, not every tool. `grep`, `find`, and `ls` are valid explicit tools and are the `ro` preset.",
|
|
238
239
|
'- `tasks` is an array. The tool recovers common stringified calls for compatibility, but canonical calls use `{ tasks: [{ prompt: "..." }] }`.',
|
|
239
240
|
'- Use `agent: "default"` for the parent\'s live model/thinking/native tools/base prompt. Built-ins are `default`, `scout`, `coder`, and `reviewer`; omitting `agent` creates an ad-hoc task.',
|
|
@@ -242,13 +243,9 @@ export function getSubagentManualMarkdown(
|
|
|
242
243
|
`- Sync \`delegate\` runs at most ${getMaxConcurrent()} tasks at once (the rest queue, not fail). Use \`async: true\` to move work to the background.`,
|
|
243
244
|
"- `deadlineMs` is a per-task wall-clock budget measured from when the task starts running (after queuing). It requests cooperative abort and is not a hard kill; completed writes/commands remain. Omission disables the deadline.",
|
|
244
245
|
"",
|
|
245
|
-
"## Legacy `action` compatibility",
|
|
246
|
-
"",
|
|
247
|
-
"The overloaded `action` field was split into `ticketAction` (poll/wait/cancel) and `sessionAction` (prompt/close/list). Legacy `action` values are still accepted at runtime through automatic normalization, but new calls should use the canonical fields. Programmatic TypeScript consumers should note the exported type `DelegateAction` is now `TicketAction`.",
|
|
248
|
-
"",
|
|
249
246
|
"## Config",
|
|
250
247
|
"",
|
|
251
|
-
"Tunables live in `~/.pi/agent/delegate.json
|
|
248
|
+
"Tunables live in `~/.pi/agent/delegate.json` (user scope, global — no project-level config): `maxConcurrent` (sync ceiling), `maxAsyncTickets` (background ticket cap), `stallTimeoutMs` (inactivity watchdog; default 900000, 0 disables), per-model/per-provider concurrency limits, legacy custom-agent model overrides (`agent`), and agent model/thinking/tools overrides — `agentOverrides` and `agentOverridesByParentModel` (exact `provider/model-id` key of the parent model; wins over `agentOverrides` on match). Config edits apply from the next delegate call.",
|
|
252
249
|
"The inactivity watchdog requests cooperative `AgentSession.abort()` cancellation and waits for the subagent to become idle; it is not a hard wall-clock execution deadline.",
|
|
253
250
|
"",
|
|
254
251
|
`Output bounding: subagent outputs longer than ${OUTPUT_SPILL_THRESHOLD_CHARS} characters are spilled to a temp file, and only the last ${OUTPUT_SPILL_TAIL_CHARS} characters stay in the LLM-facing result. Adjust with \`output.spillThresholdChars\` and \`output.spillTailChars\`. Spill files are written to the system temp directory with owner-only permissions; the full output is always available in the expanded TUI view and the spilled file.`,
|
package/package.json
CHANGED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single seam onto Pi's package-source grammar.
|
|
3
|
+
*
|
|
4
|
+
* Provider-extension verification needs three facts about a configured source:
|
|
5
|
+
* whether it is a local path, which Git repository and ref it pins, and which
|
|
6
|
+
* npm version range it requires. All three come from
|
|
7
|
+
* `DefaultPackageManager.parseSource`, which is private upstream.
|
|
8
|
+
*
|
|
9
|
+
* Reaching into that private is not the hazard — *duplicating* it is. This
|
|
10
|
+
* module exists because the previous implementation hand-rolled its own
|
|
11
|
+
* `isLocalPath`, its own npm-spec regex, and its own Git URL grammar in order
|
|
12
|
+
* to avoid the cast, and all three drifted from the originals:
|
|
13
|
+
*
|
|
14
|
+
* - the local check lowercased its input, so `NPM:pkg` was "managed" to us
|
|
15
|
+
* and "local" to Pi;
|
|
16
|
+
* - the npm regex was a verbatim copy of Pi's private `parseNpmSpec`, kept in
|
|
17
|
+
* sync by hand;
|
|
18
|
+
* - the Git parser knew nothing of `hosted-git-info`, which Pi's real parser
|
|
19
|
+
* uses for every hosted provider.
|
|
20
|
+
*
|
|
21
|
+
* A second grammar that must track a private one on every upgrade is strictly
|
|
22
|
+
* worse than one narrow cast that fails loudly. It is also not buying any
|
|
23
|
+
* independence: `getInstalledPath` — a *public* method the resolution path
|
|
24
|
+
* already depends on — calls this same private parser internally, so if
|
|
25
|
+
* `parseSource` disappears, path resolution is broken with or without this
|
|
26
|
+
* module.
|
|
27
|
+
*
|
|
28
|
+
* Hence: one cast, one module, one error type. Callers deal in the parsed
|
|
29
|
+
* result and never see Pi's raw shape.
|
|
30
|
+
*/
|
|
31
|
+
import type { DefaultPackageManager } from "@earendil-works/pi-coding-agent";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Pi's private parser is missing, threw, or returned a shape this version of
|
|
35
|
+
* delegate cannot trust.
|
|
36
|
+
*
|
|
37
|
+
* Callers fail closed on this rather than falling back to a guess: an
|
|
38
|
+
* unverifiable source is treated exactly like a source that failed
|
|
39
|
+
* verification. Callers that wrap it in a friendlier message must preserve it
|
|
40
|
+
* as `cause`, so a Pi-version mismatch stays distinguishable from a genuinely
|
|
41
|
+
* mismatched checkout.
|
|
42
|
+
*/
|
|
43
|
+
export class PiPackageSourceError extends Error {
|
|
44
|
+
/**
|
|
45
|
+
* Which kind of source Pi had already classified when the contract broke, if
|
|
46
|
+
* it got that far. Callers use it to report the failure in terms of the
|
|
47
|
+
* verification the user configured ("this Git source could not be verified")
|
|
48
|
+
* rather than as an unrelated internal error.
|
|
49
|
+
*/
|
|
50
|
+
readonly sourceType?: "git" | "npm";
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
message: string,
|
|
54
|
+
options?: { cause?: unknown; sourceType?: "git" | "npm" },
|
|
55
|
+
) {
|
|
56
|
+
super(message, { cause: options?.cause });
|
|
57
|
+
this.name = "PiPackageSourceError";
|
|
58
|
+
this.sourceType = options?.sourceType;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A Git repository identity, normalized for comparison. */
|
|
63
|
+
export interface GitRepositoryIdentity {
|
|
64
|
+
/** Lowercased host, e.g. `github.com`. */
|
|
65
|
+
host: string;
|
|
66
|
+
/** Repository path, e.g. `example/extension`. */
|
|
67
|
+
path: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface GitPackageSource extends GitRepositoryIdentity {
|
|
71
|
+
type: "git";
|
|
72
|
+
/** Configured ref. Present exactly when Pi reports the source as pinned. */
|
|
73
|
+
ref?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface NpmPackageSource {
|
|
77
|
+
type: "npm";
|
|
78
|
+
/** Version, range, or dist-tag text after the package name, if any. */
|
|
79
|
+
version?: string;
|
|
80
|
+
/** Semver range. Undefined when `version` is a dist-tag such as `latest`. */
|
|
81
|
+
range?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Whether the package installed at `installedPath` satisfies this source,
|
|
84
|
+
* using Pi's own semver comparison rather than a reimplementation. Resolves
|
|
85
|
+
* `true` for an unconstrained source, matching Pi's install-time behavior.
|
|
86
|
+
*/
|
|
87
|
+
satisfiedBy(installedPath: string): Promise<boolean>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface LocalPackageSource {
|
|
91
|
+
type: "local";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type PiPackageSource =
|
|
95
|
+
GitPackageSource | NpmPackageSource | LocalPackageSource;
|
|
96
|
+
|
|
97
|
+
type PackageManagerInternals = {
|
|
98
|
+
parseSource(source: string): unknown;
|
|
99
|
+
installedNpmMatchesConfiguredVersion(
|
|
100
|
+
parsed: unknown,
|
|
101
|
+
installedPath: string,
|
|
102
|
+
): Promise<boolean>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function internalsOf(
|
|
106
|
+
packageManager: DefaultPackageManager,
|
|
107
|
+
): Partial<PackageManagerInternals> {
|
|
108
|
+
return packageManager as unknown as Partial<PackageManagerInternals>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Normalize a Pi-parsed Git identity for equality comparison.
|
|
113
|
+
*
|
|
114
|
+
* This is comparison hygiene, not a grammar: both sides of every comparison
|
|
115
|
+
* are produced by Pi's parser, and this only removes differences Pi is
|
|
116
|
+
* indifferent to (host case, a trailing slash or `.git` that Pi strips in some
|
|
117
|
+
* source forms but not others). It never decides what a source *is*.
|
|
118
|
+
*/
|
|
119
|
+
function normalizeIdentity(
|
|
120
|
+
host: string,
|
|
121
|
+
repoPath: string,
|
|
122
|
+
): GitRepositoryIdentity | undefined {
|
|
123
|
+
const path = repoPath.replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
124
|
+
if (!host || !path) return undefined;
|
|
125
|
+
return { host: host.toLowerCase(), path };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse a configured package source with Pi's own parser.
|
|
130
|
+
*
|
|
131
|
+
* Throws `PiPackageSourceError` when the parser is unavailable or returns a
|
|
132
|
+
* result whose security-relevant fields (Git pin state, npm range) cannot be
|
|
133
|
+
* trusted. Unrecognized sources come back as `local`, which is exactly how Pi
|
|
134
|
+
* itself resolves them.
|
|
135
|
+
*/
|
|
136
|
+
export function parsePackageSource(
|
|
137
|
+
packageManager: DefaultPackageManager,
|
|
138
|
+
source: string,
|
|
139
|
+
): PiPackageSource {
|
|
140
|
+
const internals = internalsOf(packageManager);
|
|
141
|
+
if (typeof internals.parseSource !== "function") {
|
|
142
|
+
throw new PiPackageSourceError(
|
|
143
|
+
"This Pi version no longer exposes the package-source parser that delegate uses to verify provider extensions.",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let parsed: unknown;
|
|
148
|
+
try {
|
|
149
|
+
parsed = internals.parseSource.call(packageManager, source);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw new PiPackageSourceError(
|
|
152
|
+
"Pi could not parse a configured provider extension source.",
|
|
153
|
+
{ cause: error },
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
157
|
+
throw new PiPackageSourceError(
|
|
158
|
+
"Pi's package-source parser returned an unusable result for a configured provider extension.",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const record = parsed as {
|
|
163
|
+
type?: unknown;
|
|
164
|
+
host?: unknown;
|
|
165
|
+
path?: unknown;
|
|
166
|
+
ref?: unknown;
|
|
167
|
+
pinned?: unknown;
|
|
168
|
+
version?: unknown;
|
|
169
|
+
range?: unknown;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (record.type === "git") return toGitSource(record);
|
|
173
|
+
if (record.type === "npm") {
|
|
174
|
+
return toNpmSource(record, parsed, packageManager);
|
|
175
|
+
}
|
|
176
|
+
return { type: "local" };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function toGitSource(record: {
|
|
180
|
+
host?: unknown;
|
|
181
|
+
path?: unknown;
|
|
182
|
+
ref?: unknown;
|
|
183
|
+
pinned?: unknown;
|
|
184
|
+
}): GitPackageSource {
|
|
185
|
+
if (typeof record.host !== "string" || typeof record.path !== "string") {
|
|
186
|
+
throw new PiPackageSourceError(
|
|
187
|
+
"Pi's package-source parser reported a Git source without a usable repository identity.",
|
|
188
|
+
{ sourceType: "git" },
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const identity = normalizeIdentity(record.host, record.path);
|
|
192
|
+
if (!identity) {
|
|
193
|
+
throw new PiPackageSourceError(
|
|
194
|
+
"Pi's package-source parser reported an empty Git repository identity.",
|
|
195
|
+
{ sourceType: "git" },
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// `pinned` and `ref` are a security contract, not decoration. If a Pi
|
|
200
|
+
// upgrade drops either, never reinterpret a pinned source as an unpinned
|
|
201
|
+
// checkout and silently skip commit validation.
|
|
202
|
+
if (typeof record.pinned !== "boolean") {
|
|
203
|
+
throw new PiPackageSourceError(
|
|
204
|
+
"Pi's package-source parser no longer reports Git pin state.",
|
|
205
|
+
{ sourceType: "git" },
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (!record.pinned) {
|
|
209
|
+
if (record.ref !== undefined) {
|
|
210
|
+
throw new PiPackageSourceError(
|
|
211
|
+
"Pi's package-source parser reported an unpinned Git source that still carries a ref.",
|
|
212
|
+
{ sourceType: "git" },
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return { type: "git", ...identity };
|
|
216
|
+
}
|
|
217
|
+
if (typeof record.ref !== "string" || record.ref.length === 0) {
|
|
218
|
+
throw new PiPackageSourceError(
|
|
219
|
+
"Pi's package-source parser reported a pinned Git source without a ref.",
|
|
220
|
+
{ sourceType: "git" },
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return { type: "git", ...identity, ref: record.ref };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function toNpmSource(
|
|
227
|
+
record: { version?: unknown; range?: unknown },
|
|
228
|
+
parsed: unknown,
|
|
229
|
+
packageManager: DefaultPackageManager,
|
|
230
|
+
): NpmPackageSource {
|
|
231
|
+
if (record.version !== undefined && typeof record.version !== "string") {
|
|
232
|
+
throw new PiPackageSourceError(
|
|
233
|
+
"Pi's package-source parser reported an unusable npm version specifier.",
|
|
234
|
+
{ sourceType: "npm" },
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (record.range !== undefined && typeof record.range !== "string") {
|
|
238
|
+
throw new PiPackageSourceError(
|
|
239
|
+
"Pi's package-source parser reported an unusable npm version range.",
|
|
240
|
+
{ sourceType: "npm" },
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
type: "npm",
|
|
246
|
+
version: record.version,
|
|
247
|
+
range: record.range,
|
|
248
|
+
async satisfiedBy(installedPath: string): Promise<boolean> {
|
|
249
|
+
const compare =
|
|
250
|
+
internalsOf(packageManager).installedNpmMatchesConfiguredVersion;
|
|
251
|
+
if (typeof compare !== "function") {
|
|
252
|
+
throw new PiPackageSourceError(
|
|
253
|
+
"This Pi version no longer exposes the npm version check that delegate uses to verify provider extensions.",
|
|
254
|
+
{ sourceType: "npm" },
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
// Pi's own parsed object is handed back untouched: it owns the contract
|
|
258
|
+
// between its parser and its comparison, and a reconstructed stand-in
|
|
259
|
+
// would silently change meaning if that contract grew a field.
|
|
260
|
+
return compare.call(packageManager, parsed, installedPath);
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Read the repository identity of an installed checkout from its origin URL,
|
|
267
|
+
* using the same parser the configured source went through.
|
|
268
|
+
*
|
|
269
|
+
* The `git:` prefix is what makes that possible: without it Pi only accepts
|
|
270
|
+
* explicit protocol URLs, while with it Pi accepts every shorthand it accepts
|
|
271
|
+
* in a configured source (SCP `git@host:path`, bare `host/path`). Prefixing a
|
|
272
|
+
* URL that already carries a scheme is harmless — Pi strips the prefix before
|
|
273
|
+
* parsing. Returns undefined for an origin Pi does not recognize as Git, which
|
|
274
|
+
* callers treat as a failed identity match.
|
|
275
|
+
*/
|
|
276
|
+
export function parseGitOriginIdentity(
|
|
277
|
+
packageManager: DefaultPackageManager,
|
|
278
|
+
originUrl: string,
|
|
279
|
+
): GitRepositoryIdentity | undefined {
|
|
280
|
+
const trimmed = originUrl.trim();
|
|
281
|
+
if (!trimmed) return undefined;
|
|
282
|
+
const parsed = parsePackageSource(packageManager, `git:${trimmed}`);
|
|
283
|
+
if (parsed.type !== "git") return undefined;
|
|
284
|
+
return { host: parsed.host, path: parsed.path };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Whether two Pi-parsed Git identities name the same repository. */
|
|
288
|
+
export function sameRepository(
|
|
289
|
+
a: GitRepositoryIdentity,
|
|
290
|
+
b: GitRepositoryIdentity,
|
|
291
|
+
): boolean {
|
|
292
|
+
return a.host === b.host && a.path === b.path;
|
|
293
|
+
}
|
package/pool.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface FrozenConfig {
|
|
|
18
18
|
thinking: ThinkingLevel;
|
|
19
19
|
tools: string[];
|
|
20
20
|
cwd: string;
|
|
21
|
+
/** Stable signature of the provider-scoped extension allowlist this session
|
|
22
|
+
* was built with. A change in `delegate.json` providerExtensions must force
|
|
23
|
+
* session recreation so the old executable runtime is not silently reused. */
|
|
24
|
+
providerExtensions?: string;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
/** The subset a reuse request supplies for validation. `model` and
|
|
@@ -29,12 +33,20 @@ export interface ConfigCandidate {
|
|
|
29
33
|
tools: string[];
|
|
30
34
|
model?: Model<Api>;
|
|
31
35
|
systemPrompt?: string;
|
|
36
|
+
/** Provider-scoped extension allowlist signature for the current dispatch. */
|
|
37
|
+
providerExtensions?: string;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
/** One field-level diff from a reuse that conflicts with the frozen config. The
|
|
35
41
|
* pool computes these; the caller formats the error string. */
|
|
36
42
|
export interface ConfigMismatch {
|
|
37
|
-
field:
|
|
43
|
+
field:
|
|
44
|
+
| "cwd"
|
|
45
|
+
| "thinking"
|
|
46
|
+
| "tools"
|
|
47
|
+
| "model"
|
|
48
|
+
| "systemPrompt"
|
|
49
|
+
| "providerExtensions";
|
|
38
50
|
frozen: string;
|
|
39
51
|
requested: string;
|
|
40
52
|
}
|
|
@@ -189,6 +201,16 @@ export function checkout(
|
|
|
189
201
|
requested: "<requested>",
|
|
190
202
|
});
|
|
191
203
|
}
|
|
204
|
+
if (frozen.providerExtensions !== candidate.providerExtensions) {
|
|
205
|
+
// This field is derived from configured package sources, which may contain
|
|
206
|
+
// credentials. Never expose either the sources or their digest through the
|
|
207
|
+
// public checkout result; a digest can still enable dictionary guessing.
|
|
208
|
+
mismatches.push({
|
|
209
|
+
field: "providerExtensions",
|
|
210
|
+
frozen: "<redacted>",
|
|
211
|
+
requested: "<redacted>",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
192
214
|
if (mismatches.length) return { status: "mismatch", mismatches };
|
|
193
215
|
|
|
194
216
|
return {
|