sortie-dogs 0.12.11 → 0.12.13
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/asset-version.d.ts +1 -1
- package/dist/asset-version.js +1 -1
- package/dist/core/goal-bound.d.ts +10 -1
- package/dist/core/goal-bound.js +16 -1
- package/dist/core/operator-mission.d.ts +3 -1
- package/dist/core/operator-mission.js +16 -6
- package/dist/core/operator-runtime.d.ts +7 -3
- package/dist/core/operator-runtime.js +31 -13
- package/dist/core/path.d.ts +2 -0
- package/dist/core/path.js +5 -0
- package/dist/core/run-flight-ledger.js +4 -0
- package/dist/plugin/declared-artifacts.d.ts +8 -0
- package/dist/plugin/declared-artifacts.js +80 -0
- package/dist/plugin/gate.js +6 -11
- package/dist/plugin/index.js +115 -51
- package/dist/plugin/mission-review.js +22 -6
- package/dist/plugin/profiled.js +121 -22
- package/dist/plugin/protected-snapshot.js +27 -5
- package/dist/plugin/runtime-bridge.d.ts +8 -0
- package/dist/plugin/unit-usage.d.ts +2 -0
- package/dist/plugin/unit-usage.js +44 -0
- package/dist/plugin/v2.js +7 -0
- package/dist/runtime-mission-assets.js +19 -3
- package/package.json +1 -1
package/dist/asset-version.d.ts
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export declare const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export declare const V010_RUNTIME_ASSET_VERSION = "0.12.
|
|
6
|
+
export declare const V010_RUNTIME_ASSET_VERSION = "0.12.13-mission-cold-recovery-v1";
|
|
7
7
|
export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION | typeof V010_RUNTIME_ASSET_VERSION;
|
package/dist/asset-version.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* installed project marker without importing every asset body.
|
|
4
4
|
*/
|
|
5
5
|
export const RUNTIME_ASSET_VERSION = "0.3.89-completion-proof-v1";
|
|
6
|
-
export const V010_RUNTIME_ASSET_VERSION = "0.12.
|
|
6
|
+
export const V010_RUNTIME_ASSET_VERSION = "0.12.13-mission-cold-recovery-v1";
|
|
@@ -38,7 +38,7 @@ export interface GoalEvidence {
|
|
|
38
38
|
readonly source_paths: readonly string[];
|
|
39
39
|
readonly candidate_paths: readonly string[];
|
|
40
40
|
/** Missing on legacy evidence: retain its original all-paths snapshot recipe. */
|
|
41
|
-
readonly source_policy?: "project-files-v1";
|
|
41
|
+
readonly source_policy?: "project-files-v1" | "declared-paths-v1";
|
|
42
42
|
};
|
|
43
43
|
readonly execution: {
|
|
44
44
|
readonly command: readonly string[];
|
|
@@ -167,6 +167,15 @@ export type GoalFlightEvent = (GoalEventBase & {
|
|
|
167
167
|
readonly evidence: readonly GoalEvidence[];
|
|
168
168
|
readonly elapsed_ms: number | null;
|
|
169
169
|
readonly cost_usd: number | null;
|
|
170
|
+
readonly native_session_id?: string;
|
|
171
|
+
readonly native_started_at?: string;
|
|
172
|
+
}) | (GoalEventBase & {
|
|
173
|
+
readonly kind: "unit.usage-reconciled";
|
|
174
|
+
readonly goal_id: string;
|
|
175
|
+
readonly reservation_id: string;
|
|
176
|
+
readonly native_session_id: string;
|
|
177
|
+
readonly cost_usd: number;
|
|
178
|
+
readonly source: "native-usage-price-table";
|
|
170
179
|
}) | (GoalEventBase & {
|
|
171
180
|
readonly kind: "unit.evidence-reconciled";
|
|
172
181
|
readonly goal_id: string;
|
package/dist/core/goal-bound.js
CHANGED
|
@@ -62,7 +62,8 @@ export function validGoalEvidence(value, state) {
|
|
|
62
62
|
text(protectedBinding.project_root) && text(protectedBinding.manifest_path) && Array.isArray(protectedBinding.source_paths) &&
|
|
63
63
|
protectedBinding.source_paths.every(text) && Array.isArray(protectedBinding.candidate_paths) &&
|
|
64
64
|
protectedBinding.candidate_paths.every(text) &&
|
|
65
|
-
(protectedBinding.source_policy === undefined || protectedBinding.source_policy === "project-files-v1"
|
|
65
|
+
(protectedBinding.source_policy === undefined || protectedBinding.source_policy === "project-files-v1" ||
|
|
66
|
+
protectedBinding.source_policy === "declared-paths-v1");
|
|
66
67
|
const matches = criteria.length > 0 && criteria.length === value.measurement.criterion_ids.length &&
|
|
67
68
|
criteria.every((criterion) => criterion.target === value.measurement.target &&
|
|
68
69
|
criterion.entrypoint === value.measurement.entrypoint && criterion.workload === value.measurement.workload &&
|
|
@@ -114,6 +115,7 @@ export function reduceGoalFlight(records) {
|
|
|
114
115
|
let legacyResetRevision = false;
|
|
115
116
|
let previous = null;
|
|
116
117
|
const processDefects = new Map();
|
|
118
|
+
const unitCosts = new Map();
|
|
117
119
|
const validationAdmissions = new Map();
|
|
118
120
|
const validationSettlements = new Map();
|
|
119
121
|
const reopenedValidationEvidence = new Set();
|
|
@@ -127,6 +129,7 @@ export function reduceGoalFlight(records) {
|
|
|
127
129
|
continue;
|
|
128
130
|
requireState(instant(event.at), "invalid", "Goal event timestamp is invalid.");
|
|
129
131
|
if (event.kind === "goal.accepted") {
|
|
132
|
+
unitCosts.clear();
|
|
130
133
|
requireState(state.goal_id === null || state.phase === "terminal" || state.phase === "stopped", "transition", "An active goal already owns this root.");
|
|
131
134
|
requireState(text(event.goal_id) && HASH.test(event.acceptance_fingerprint) && text(event.origin_user_message_id) &&
|
|
132
135
|
text(event.origin_session_id) && text(event.selected_agent) && event.budget.max_units > 0 &&
|
|
@@ -221,6 +224,7 @@ export function reduceGoalFlight(records) {
|
|
|
221
224
|
: state.no_progress_results;
|
|
222
225
|
const elapsed = state.consumed_time_ms === null || event.elapsed_ms === null ? null : state.consumed_time_ms + event.elapsed_ms;
|
|
223
226
|
const cost = state.consumed_cost_usd === null || event.cost_usd === null ? null : state.consumed_cost_usd + event.cost_usd;
|
|
227
|
+
unitCosts.set(event.reservation_id, { cost: event.cost_usd, session: event.native_session_id });
|
|
224
228
|
state = { ...state, consumed_units: state.consumed_units + 1, consumed_time_ms: elapsed, consumed_cost_usd: cost,
|
|
225
229
|
no_progress_results: nextNoProgress, replan_required: nextNoProgress >= 2,
|
|
226
230
|
outstanding_reservations: state.outstanding_reservations.filter((entry) => entry.reservation_id !== event.reservation_id),
|
|
@@ -231,6 +235,17 @@ export function reduceGoalFlight(records) {
|
|
|
231
235
|
processDefects.set(event.receipt_id, { unitID: event.unit_id, revision: state.revision, epoch: state.scope_epoch });
|
|
232
236
|
}
|
|
233
237
|
}
|
|
238
|
+
else if (event.kind === "unit.usage-reconciled") {
|
|
239
|
+
const prior = unitCosts.get(event.reservation_id);
|
|
240
|
+
requireState(event.goal_id === state.goal_id && prior !== undefined && prior.session === event.native_session_id &&
|
|
241
|
+
event.source === "native-usage-price-table" &&
|
|
242
|
+
Number.isFinite(event.cost_usd) && event.cost_usd >= 0 &&
|
|
243
|
+
(prior.cost === null || prior.cost === event.cost_usd), "transition", "Usage reconciliation must identify one settled native unit.");
|
|
244
|
+
unitCosts.set(event.reservation_id, { cost: event.cost_usd, session: event.native_session_id });
|
|
245
|
+
const costs = [...unitCosts.values()];
|
|
246
|
+
state = { ...state, consumed_cost_usd: costs.some(item => item.cost === null) ? null
|
|
247
|
+
: costs.reduce((sum, item) => sum + item.cost, 0) };
|
|
248
|
+
}
|
|
234
249
|
else if (event.kind === "unit.evidence-reconciled") {
|
|
235
250
|
const old = processDefects.get(event.previous_receipt_id);
|
|
236
251
|
requireState(state.phase === "active" && state.receipt === null && old?.unitID === event.unit_id &&
|
|
@@ -17,6 +17,8 @@ export interface OperatorMission {
|
|
|
17
17
|
id: string;
|
|
18
18
|
text: string;
|
|
19
19
|
}[];
|
|
20
|
+
/** The current user intentionally replaced the predecessor's requirements. */
|
|
21
|
+
requirementsReplaced?: boolean;
|
|
20
22
|
phase: "open" | "running" | "submitted" | "completed" | "cancelled";
|
|
21
23
|
coordinator: string | null;
|
|
22
24
|
callID: string | null;
|
|
@@ -74,7 +76,7 @@ export declare class OperatorMissionRuntime {
|
|
|
74
76
|
read(root: string): Promise<OperatorMission | undefined>;
|
|
75
77
|
required(root: string): Promise<OperatorMission>;
|
|
76
78
|
capture(root: string, request: MissionRequest): Promise<void>;
|
|
77
|
-
start(root: string, requirements: unknown): Promise<OperatorMission>;
|
|
79
|
+
start(root: string, requirements: unknown, replaceRequirements?: boolean): Promise<OperatorMission>;
|
|
78
80
|
update(root: string, change: (state: OperatorMission) => void): Promise<OperatorMission>;
|
|
79
81
|
/** Keep host-accepted criteria ahead of new text, including an exactly linked settled predecessor. */
|
|
80
82
|
carryForward(root: string, missionID: string, acceptance: readonly string[], supersededRunID?: string): Promise<OperatorMission>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { normalizeExecutionScope } from "./path.js";
|
|
5
5
|
import { parseOperatorPlan } from "./operator-runtime.js";
|
|
6
6
|
import { profileAgent } from "./runtime-profile.js";
|
|
7
7
|
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
@@ -160,7 +160,7 @@ export class OperatorMissionRuntime {
|
|
|
160
160
|
}
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
|
-
start(root, requirements) {
|
|
163
|
+
start(root, requirements, replaceRequirements = false) {
|
|
164
164
|
return this.serial(root, async () => {
|
|
165
165
|
if (!Array.isArray(requirements) || requirements.length === 0 || requirements.length > 64 ||
|
|
166
166
|
!requirements.every(item => typeof item === "string" && item.trim() && !/[\r\n]/u.test(item))) {
|
|
@@ -182,6 +182,7 @@ export class OperatorMissionRuntime {
|
|
|
182
182
|
await this.save(this.file(root, `.${previous.id}`), previous);
|
|
183
183
|
const state = { version: "0.12", id: `mission-${randomUUID()}`, root, requests: [request],
|
|
184
184
|
requirements: requirements.map((text, index) => ({ id: `R${index + 1}`, text })), phase: "open",
|
|
185
|
+
...(replaceRequirements ? { requirementsReplaced: true } : {}),
|
|
185
186
|
coordinator: null, callID: null, dispatchOpen: false, runID: null, plans: 0, progress: [], submission: null,
|
|
186
187
|
...(previous?.phase === "cancelled" &&
|
|
187
188
|
(previous.runID === null || request.id !== previous.requests[0]?.id) &&
|
|
@@ -286,7 +287,7 @@ export function missionPlan(mission, raw) {
|
|
|
286
287
|
const entries = value[field] ?? [];
|
|
287
288
|
if (!Array.isArray(entries) || !entries.every(item => typeof item === "string"))
|
|
288
289
|
throw new Error(`mission-unit-${index + 1}: ${field} must be paths`);
|
|
289
|
-
return [...new Set(entries.map(item =>
|
|
290
|
+
return [...new Set(entries.map(item => normalizeExecutionScope(item)))];
|
|
290
291
|
};
|
|
291
292
|
const ids = value.requirement_ids ?? mission.requirements.map(item => item.id);
|
|
292
293
|
if (!Array.isArray(ids) || ids.length === 0 || !ids.every(id => mission.requirements.some(item => item.id === id))) {
|
|
@@ -328,12 +329,18 @@ export function missionPlan(mission, raw) {
|
|
|
328
329
|
// Keep all detailed instructions in the handoff, not in a 512-character wire label.
|
|
329
330
|
criteria: units.map(unit => ({ criterion_id: unit.id, target: unit.title.slice(0, 512),
|
|
330
331
|
entrypoint: (unit.write[0] ?? unit.read[0] ?? unit.id).slice(0, 512), validation_command: unit.validation.at(-1) })) },
|
|
331
|
-
units });
|
|
332
|
+
units }, "execution");
|
|
332
333
|
}
|
|
333
334
|
export function missionPacket(mission, run) {
|
|
335
|
+
const predecessor = run && mission.runID !== run.runID && (mission.supersededRunID === run.runID || run.phase === "cancelled") ? run : undefined;
|
|
336
|
+
if (predecessor)
|
|
337
|
+
run = undefined;
|
|
334
338
|
const currentReview = mission.review !== undefined && mission.review.runID === (run?.runID ?? mission.runID);
|
|
335
339
|
const reviewAccepted = currentReview && missionReviewAccepted(mission.review);
|
|
336
340
|
return { mission_id: mission.id, phase: mission.phase, coordinator_session_id: mission.coordinator,
|
|
341
|
+
...(predecessor ? { predecessor: { run_id: predecessor.runID, status: predecessor.phase,
|
|
342
|
+
completed_units: predecessor.units.filter(unit => unit.status === "succeeded").length,
|
|
343
|
+
note: "Historical results and spend are retained; they do not complete the current requirements." } } : {}),
|
|
337
344
|
requirements: mission.requirements, original_request_refs: mission.requests.map(item => `user:${item.id}`),
|
|
338
345
|
submission: mission.submission, progress: mission.progress,
|
|
339
346
|
execution_summary: { completed_units: run?.units.filter(unit => unit.status === "succeeded").length ?? 0,
|
|
@@ -350,11 +357,14 @@ export function missionPacket(mission, run) {
|
|
|
350
357
|
passed: currentReview && mission.review.verdict === "PASS", permits_submission: reviewAccepted } : null,
|
|
351
358
|
...(run ? { run_id: run.runID, status: run.phase, decision: run.decision,
|
|
352
359
|
units: run.units.map(unit => ({ id: unit.unit.id, title: unit.unit.title, status: unit.status,
|
|
353
|
-
child_session_id: unit.childSessionID, result_class: unit.resultClass, evidence: unit.evidence
|
|
360
|
+
child_session_id: unit.childSessionID, result_class: unit.resultClass, evidence: unit.evidence,
|
|
361
|
+
...(unit.dispatchDenial ? { dispatch_denial: unit.dispatchDenial } : {}) })) } : {}),
|
|
354
362
|
next_action: mission.phase === "completed" ? "Mission completed. Report the accepted result and retained review gaps; no further dispatch or completion call is needed."
|
|
355
363
|
: mission.phase === "submitted" && mission.submission?.status === "ready"
|
|
356
364
|
? "Operator: compare the submitted candidate with the original requirements and actual evidence, then complete_mission if satisfied. Report remaining evidence gaps; they are not a review PASS."
|
|
357
|
-
: run?.phase === "awaiting-decision" ?
|
|
365
|
+
: run?.phase === "awaiting-decision" ? (run.units.some(unit => unit.dispatchDenial)
|
|
366
|
+
? "Coordinator: inspect units[].dispatch_denial before changing the plan. Correct only its diagnosed cause; do not repeat an unchanged refused Task or replan for a host-state mismatch. Report an unresolved runtime mismatch with the loaded runtime identity; preserve requirements and cumulative spend."
|
|
367
|
+
: "Coordinator: correct the cause and call plan_units with the remaining work and all requirements; budget is cumulative.")
|
|
358
368
|
: run?.phase === "awaiting-acceptance" ? (currentReview && mission.review?.verdict === "evidence-gaps" && !reviewAccepted
|
|
359
369
|
? "Coordinator: the Reviewer found only missing evidence. Supply it through review_mission traces (or at most one evidence-only unit) and re-review once; do not re-implement. At the evidence-gap limit, submit_mission ready with the gaps listed."
|
|
360
370
|
: reviewAccepted
|
|
@@ -115,6 +115,7 @@ interface UnitState {
|
|
|
115
115
|
childSessionID: string | null;
|
|
116
116
|
evidence: readonly GoalEvidence[];
|
|
117
117
|
resultClass: string | null;
|
|
118
|
+
dispatchDenial?: string;
|
|
118
119
|
repairValidationAttempts: number;
|
|
119
120
|
repairValidation: {
|
|
120
121
|
readonly repair_fingerprint: string;
|
|
@@ -203,6 +204,8 @@ export interface OperatorState {
|
|
|
203
204
|
operatorCallID: string | null;
|
|
204
205
|
operatorSessionID: string | null;
|
|
205
206
|
dispatched: number;
|
|
207
|
+
/** Native interrupt acknowledgements for this cancelled run, persisted before settlement. */
|
|
208
|
+
stoppedChildren?: string[];
|
|
206
209
|
units: UnitState[];
|
|
207
210
|
decision: string | null;
|
|
208
211
|
receipt: GoalTerminalReceipt | null;
|
|
@@ -210,7 +213,7 @@ export interface OperatorState {
|
|
|
210
213
|
/** A later mission may replace an unexecuted cancellation, but must retain settled work's acceptance. */
|
|
211
214
|
export declare function cancelledMissionRetainsAcceptance(state: OperatorState): boolean;
|
|
212
215
|
export declare function operatorGitPathAuthorized(path: string, scopes: readonly string[], platform?: NodeJS.Platform): boolean;
|
|
213
|
-
export declare function parseOperatorPlan(value: unknown): OperatorPlan;
|
|
216
|
+
export declare function parseOperatorPlan(value: unknown, scopeFormat?: "repository" | "execution"): OperatorPlan;
|
|
214
217
|
/** Root-owned durable queue. It never executes code, changes acceptance, or accepts a candidate. */
|
|
215
218
|
export declare class OperatorRuntime {
|
|
216
219
|
readonly profile: RuntimeProfile;
|
|
@@ -247,7 +250,7 @@ export declare class OperatorRuntime {
|
|
|
247
250
|
prepareMission(root: string, raw: unknown, dispatcher?: {
|
|
248
251
|
sessionID: string;
|
|
249
252
|
callID: string;
|
|
250
|
-
}, supersededRunID?: string, terminalChildren?: readonly string[]): Promise<OperatorState>;
|
|
253
|
+
}, supersededRunID?: string, terminalChildren?: readonly string[], replaceRequirements?: boolean): Promise<OperatorState>;
|
|
251
254
|
/** Stage a settled mission's replacement; rejected preparation never cancels the usable run. */
|
|
252
255
|
replanMission(root: string, runID: string, raw: unknown, dispatcher?: {
|
|
253
256
|
sessionID: string;
|
|
@@ -284,7 +287,7 @@ export declare class OperatorRuntime {
|
|
|
284
287
|
private nextOnce;
|
|
285
288
|
admitWorker(root: string, actor: string, callID: string, args: unknown): Promise<OperatorTask>;
|
|
286
289
|
private admitWorkerOnce;
|
|
287
|
-
rejectedAdmission(root: string, callID: string): Promise<void>;
|
|
290
|
+
rejectedAdmission(root: string, callID: string, reason?: string): Promise<void>;
|
|
288
291
|
rejectDispatch(root: string, callID: string, decision?: string): Promise<void>;
|
|
289
292
|
private rejectDispatchOnce;
|
|
290
293
|
private repairPathProtected;
|
|
@@ -322,6 +325,7 @@ export declare class OperatorRuntime {
|
|
|
322
325
|
processReplacementRequired(root: string, runID: string): Promise<OperatorState>;
|
|
323
326
|
private operatorReturnedOnce;
|
|
324
327
|
interrupted(root: string, reason: string): Promise<readonly string[]>;
|
|
328
|
+
recordStoppedChild(root: string, runID: string, child: string): Promise<void>;
|
|
325
329
|
private interruptedOnce;
|
|
326
330
|
terminal(root: string, receipt: GoalTerminalReceipt): Promise<void>;
|
|
327
331
|
private terminalOnce;
|
|
@@ -5,7 +5,7 @@ import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { acceptanceContinuityFingerprint, inspectAcceptanceContinuity, normalizeAcceptanceCriteria, ACCEPTANCE_CONTINUITY_EXTENSION, MAX_ACCEPTANCE_CONTINUITY_BYTES, MAX_ACCEPTANCE_CRITERIA } from "./acceptance-continuity.js";
|
|
7
7
|
import { expandGoalDeclaration, goalDeclarationDefaults, goalDeclarationFieldDiagnostics, hasGoalCommandAliasConflict } from "./goal-declaration-format.js";
|
|
8
|
-
import { normalizeManifestPath, normalizeManifestScope, normalizeRelativePath } from "./path.js";
|
|
8
|
+
import { normalizeExecutionScope, normalizeManifestPath, normalizeManifestScope, normalizeRelativePath } from "./path.js";
|
|
9
9
|
import { CONTRACT_TEXT_LIMITS, validateHandoffSchema, validateOperationManifestSchema } from "./validate-schema.js";
|
|
10
10
|
import { validateManifest } from "./validate-manifest.js";
|
|
11
11
|
import { profileAgent, RUNTIME_PROFILES } from "./runtime-profile.js";
|
|
@@ -83,7 +83,7 @@ function scopePaths(value, limit) {
|
|
|
83
83
|
};
|
|
84
84
|
return paths.every(normalized) && new Set(paths).size === paths.length;
|
|
85
85
|
}
|
|
86
|
-
export function parseOperatorPlan(value) {
|
|
86
|
+
export function parseOperatorPlan(value, scopeFormat = "repository") {
|
|
87
87
|
if (!record(value) || !exactKeys(value, ["schema_version", "acceptance", "acceptance_proof", "source_refs", "goal_declaration", "units", "git_lifecycle"])) {
|
|
88
88
|
return planError("/", "operator-plan-invalid", "operator-plan-fields");
|
|
89
89
|
}
|
|
@@ -207,11 +207,11 @@ export function parseOperatorPlan(value) {
|
|
|
207
207
|
for (const [pathIndex, name] of unit[field].entries()) {
|
|
208
208
|
let valid = false;
|
|
209
209
|
try {
|
|
210
|
-
valid = normalizeRelativePath(name) === name;
|
|
210
|
+
valid = (scopeFormat === "execution" ? normalizeExecutionScope(name) : normalizeRelativePath(name)) === name;
|
|
211
211
|
}
|
|
212
212
|
catch { /* Return only a typed pointer, never the path value. */ }
|
|
213
213
|
if (!valid)
|
|
214
|
-
return planError(`/units/${unitIndex}/${field}/${pathIndex}`, "operator-scope-invalid", "repository-relative-scope");
|
|
214
|
+
return planError(`/units/${unitIndex}/${field}/${pathIndex}`, "operator-scope-invalid", scopeFormat === "execution" ? "normalized-execution-scope" : "repository-relative-scope");
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
for (const name of unit.write) {
|
|
@@ -686,8 +686,8 @@ export class OperatorRuntime {
|
|
|
686
686
|
}
|
|
687
687
|
}
|
|
688
688
|
/** Mission authority is supplied only by the owning profile, never by a model-authored plan. */
|
|
689
|
-
prepareMission(root, raw, dispatcher, supersededRunID, terminalChildren = []) {
|
|
690
|
-
return this.serial(root, () => this.prepareOnce(root, raw, undefined, { dispatcher, supersededRunID, terminalChildren }));
|
|
689
|
+
prepareMission(root, raw, dispatcher, supersededRunID, terminalChildren = [], replaceRequirements = false) {
|
|
690
|
+
return this.serial(root, () => this.prepareOnce(root, raw, undefined, { dispatcher, supersededRunID, terminalChildren, replaceRequirements }));
|
|
691
691
|
}
|
|
692
692
|
/** Stage a settled mission's replacement; rejected preparation never cancels the usable run. */
|
|
693
693
|
replanMission(root, runID, raw, dispatcher) {
|
|
@@ -716,7 +716,7 @@ export class OperatorRuntime {
|
|
|
716
716
|
}
|
|
717
717
|
immutableReplacement = { ...immutableReplacement, goal_declaration: declaration };
|
|
718
718
|
}
|
|
719
|
-
const plan = parseOperatorPlan(immutableReplacement);
|
|
719
|
+
const plan = parseOperatorPlan(immutableReplacement, mission ? "execution" : "repository");
|
|
720
720
|
const validatedPlanHash = hash(JSON.stringify(plan));
|
|
721
721
|
const replanning = mission?.replaceRunID !== undefined;
|
|
722
722
|
if (replanning) {
|
|
@@ -758,12 +758,12 @@ export class OperatorRuntime {
|
|
|
758
758
|
return previous;
|
|
759
759
|
throw new Error("operator-active-contract-immutable");
|
|
760
760
|
}
|
|
761
|
-
//
|
|
762
|
-
//
|
|
761
|
+
// Same-request continuation retains acceptance. Explicit user replacement keeps the
|
|
762
|
+
// predecessor's history/spend but uses the current requirements instead of an old prefix.
|
|
763
763
|
// Treat the predecessor as retired only in this staged replacement. Its durable state remains
|
|
764
764
|
// intact through schema checks, control writes and archive creation, until save atomically switches it.
|
|
765
765
|
const parent = replanning ? { ...previous, phase: "cancelled", decision: "mission-replan" }
|
|
766
|
-
: (!superseding || retainAcceptance) && previous?.phase === "cancelled" ? previous : undefined;
|
|
766
|
+
: (!superseding || (retainAcceptance && !mission?.replaceRequirements)) && previous?.phase === "cancelled" ? previous : undefined;
|
|
767
767
|
if (parent && ["explicit-cancellation", "agent-changed"].includes(parent.decision ?? "")) {
|
|
768
768
|
const cancelled = parent.units.flatMap(unit => (parent.decision === "agent-changed" || unit.status === "cancelled") && unit.childSessionID !== null
|
|
769
769
|
? [unit.childSessionID] : []);
|
|
@@ -837,6 +837,13 @@ export class OperatorRuntime {
|
|
|
837
837
|
const units = [];
|
|
838
838
|
const controls = [{ path: declarationPath, content: declaration }];
|
|
839
839
|
for (const [index, unit] of plan.units.entries()) {
|
|
840
|
+
for (const name of unit.write) {
|
|
841
|
+
const scoped = relative(this.projectRoot, resolve(this.projectRoot, normalizeManifestScope(name).path)).replaceAll("\\", "/");
|
|
842
|
+
if ([".git", ...Object.values(RUNTIME_PROFILES).map(profile => profile.stateDirectory)]
|
|
843
|
+
.some(directory => scoped === directory || scoped.startsWith(`${directory}/`))) {
|
|
844
|
+
return planError(`/units/${index}/write`, "operator-control-write-forbidden", "control-write-forbidden");
|
|
845
|
+
}
|
|
846
|
+
}
|
|
840
847
|
const taskID = `${runID}-${index + 1}`;
|
|
841
848
|
const manifestPath = join(directory, `${taskID}.operation-manifest.json`);
|
|
842
849
|
const manifestRelative = `${this.profile.stateDirectory}/contracts/${taskID}.operation-manifest.json`;
|
|
@@ -1232,19 +1239,21 @@ export class OperatorRuntime {
|
|
|
1232
1239
|
await this.save(state);
|
|
1233
1240
|
return admitted;
|
|
1234
1241
|
}
|
|
1235
|
-
async rejectedAdmission(root, callID) {
|
|
1236
|
-
await this.
|
|
1242
|
+
async rejectedAdmission(root, callID, reason) {
|
|
1243
|
+
await this.serial(root, () => this.rejectDispatchOnce(root, callID, "dispatch-admission-rejected", reason));
|
|
1237
1244
|
}
|
|
1238
1245
|
rejectDispatch(root, callID, decision = "native-task-rejected") {
|
|
1239
1246
|
return this.serial(root, () => this.rejectDispatchOnce(root, callID, decision));
|
|
1240
1247
|
}
|
|
1241
|
-
async rejectDispatchOnce(root, callID, decision) {
|
|
1248
|
+
async rejectDispatchOnce(root, callID, decision, reason) {
|
|
1242
1249
|
const state = await this.required(root);
|
|
1243
1250
|
const unit = state.units.find(item => item.callID === callID && item.status === "running");
|
|
1244
1251
|
if (!unit)
|
|
1245
1252
|
return;
|
|
1246
1253
|
unit.status = "failed";
|
|
1247
1254
|
unit.resultClass = "process-defect";
|
|
1255
|
+
if (reason)
|
|
1256
|
+
unit.dispatchDenial = reason.slice(0, 4000);
|
|
1248
1257
|
state.phase = "awaiting-decision";
|
|
1249
1258
|
state.decision = decision;
|
|
1250
1259
|
await this.save(state);
|
|
@@ -1570,6 +1579,15 @@ export class OperatorRuntime {
|
|
|
1570
1579
|
interrupted(root, reason) {
|
|
1571
1580
|
return this.serial(root, () => this.interruptedOnce(root, reason));
|
|
1572
1581
|
}
|
|
1582
|
+
recordStoppedChild(root, runID, child) {
|
|
1583
|
+
return this.serial(root, async () => {
|
|
1584
|
+
const state = await this.required(root);
|
|
1585
|
+
if (state.runID !== runID || state.phase !== "cancelled")
|
|
1586
|
+
return;
|
|
1587
|
+
state.stoppedChildren = [...new Set([...(state.stoppedChildren ?? []), child])];
|
|
1588
|
+
await this.save(state);
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1573
1591
|
async interruptedOnce(root, reason) {
|
|
1574
1592
|
const state = await this.read(root);
|
|
1575
1593
|
if (!state || state.phase === "completed")
|
package/dist/core/path.d.ts
CHANGED
|
@@ -28,3 +28,5 @@ export declare function normalizeManifestScope(input: string): ManifestPath & {
|
|
|
28
28
|
readonly directory: boolean;
|
|
29
29
|
};
|
|
30
30
|
export declare function normalizeRelativeScope(input: string): string;
|
|
31
|
+
/** Execution scopes may name native absolute destinations (for example global installs). */
|
|
32
|
+
export declare function normalizeExecutionScope(input: string): string;
|
package/dist/core/path.js
CHANGED
|
@@ -67,3 +67,8 @@ export function normalizeRelativeScope(input) {
|
|
|
67
67
|
throw new RelativePathError("absolute");
|
|
68
68
|
return scope.path + (scope.directory ? "/**" : "");
|
|
69
69
|
}
|
|
70
|
+
/** Execution scopes may name native absolute destinations (for example global installs). */
|
|
71
|
+
export function normalizeExecutionScope(input) {
|
|
72
|
+
const scope = normalizeManifestScope(input);
|
|
73
|
+
return scope.path + (scope.directory ? "/**" : "");
|
|
74
|
+
}
|
|
@@ -918,6 +918,10 @@ export class RunFlightLedger {
|
|
|
918
918
|
const records = await this.#readGoalRecords();
|
|
919
919
|
if (guard !== undefined)
|
|
920
920
|
guard(reduceGoalFlight(records));
|
|
921
|
+
if (event.kind === "unit.usage-reconciled" && records.some(({ event: stored }) => stored.kind === "unit.usage-reconciled" && stored.goal_id === event.goal_id &&
|
|
922
|
+
stored.reservation_id === event.reservation_id && stored.native_session_id === event.native_session_id &&
|
|
923
|
+
stored.cost_usd === event.cost_usd))
|
|
924
|
+
return reduceGoalFlight(records);
|
|
921
925
|
if (event.kind === "goal.accepted") {
|
|
922
926
|
const prior = records.find(({ event: stored }) => stored.kind === "goal.accepted" &&
|
|
923
927
|
stored.origin_user_message_id === event.origin_user_message_id);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Explicit external artifacts are filesystem inputs, not Git pathspecs. Stream complete bytes;
|
|
2
|
+
* previews are bounded independently. Follow package links only inside a declared physical root.
|
|
3
|
+
*/
|
|
4
|
+
export declare function declaredArtifacts(paths: readonly string[], previewLimit?: number): Promise<{
|
|
5
|
+
entries: (readonly [string, string, (string | undefined)?])[];
|
|
6
|
+
excerpt: string;
|
|
7
|
+
truncated: boolean;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { lstat, readdir, readlink, realpath } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
+
const within = (root, path) => {
|
|
6
|
+
const rest = relative(root, path);
|
|
7
|
+
return rest === "" || (rest !== ".." && !rest.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(rest));
|
|
8
|
+
};
|
|
9
|
+
/** Explicit external artifacts are filesystem inputs, not Git pathspecs. Stream complete bytes;
|
|
10
|
+
* previews are bounded independently. Follow package links only inside a declared physical root.
|
|
11
|
+
*/
|
|
12
|
+
export async function declaredArtifacts(paths, previewLimit = 0) {
|
|
13
|
+
const roots = [...new Set(paths.map(path => resolve(path)))].sort();
|
|
14
|
+
const physical = await Promise.all(roots.map(path => realpath(path).catch((error) => {
|
|
15
|
+
if (error.code === "ENOENT")
|
|
16
|
+
return path;
|
|
17
|
+
throw error;
|
|
18
|
+
})));
|
|
19
|
+
const entries = [];
|
|
20
|
+
const excerpts = [];
|
|
21
|
+
let remaining = previewLimit, truncated = false;
|
|
22
|
+
const visited = new Set();
|
|
23
|
+
const visit = async (path, ancestors) => {
|
|
24
|
+
if (visited.has(path))
|
|
25
|
+
return;
|
|
26
|
+
visited.add(path);
|
|
27
|
+
const label = path.replaceAll("\\", "/");
|
|
28
|
+
const metadata = await lstat(path).catch((error) => {
|
|
29
|
+
if (error.code === "ENOENT")
|
|
30
|
+
return undefined;
|
|
31
|
+
throw error;
|
|
32
|
+
});
|
|
33
|
+
if (!metadata) {
|
|
34
|
+
entries.push([label, "missing"]);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const canonical = await realpath(path);
|
|
38
|
+
if (metadata.isSymbolicLink()) {
|
|
39
|
+
entries.push([label, `symlink:${await readlink(path)}`, canonical]);
|
|
40
|
+
if (!physical.some(root => within(root, canonical)))
|
|
41
|
+
throw new Error("declared-artifact-link-outside-scope");
|
|
42
|
+
if (ancestors.has(canonical))
|
|
43
|
+
return; // The link identity is recorded; do not traverse cycles.
|
|
44
|
+
await visit(canonical, ancestors);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (metadata.isDirectory()) {
|
|
48
|
+
entries.push([label, "directory", canonical]);
|
|
49
|
+
const next = new Set([...ancestors, canonical]);
|
|
50
|
+
for (const child of (await readdir(path)).sort())
|
|
51
|
+
await visit(join(path, child), next);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!metadata.isFile())
|
|
55
|
+
throw new Error("declared-artifact-not-file-or-directory");
|
|
56
|
+
const hash = createHash("sha256");
|
|
57
|
+
const room = remaining;
|
|
58
|
+
let preview = Buffer.alloc(0);
|
|
59
|
+
for await (const part of createReadStream(path)) {
|
|
60
|
+
const bytes = Buffer.isBuffer(part) ? part : Buffer.from(part);
|
|
61
|
+
hash.update(bytes);
|
|
62
|
+
if (preview.length < room)
|
|
63
|
+
preview = Buffer.concat([preview, bytes.subarray(0, room - preview.length)]);
|
|
64
|
+
}
|
|
65
|
+
entries.push([label, `file:${metadata.mode}`, hash.digest("hex")]);
|
|
66
|
+
if (previewLimit > 0) {
|
|
67
|
+
if (room) {
|
|
68
|
+
const excerpt = `\n--- external file: ${label} ---\n${preview.includes(0) ? "[binary artifact: bytes fingerprinted]" : preview.toString("utf8")}`;
|
|
69
|
+
const bounded = Buffer.from(excerpt).subarray(0, remaining);
|
|
70
|
+
excerpts.push(bounded.toString("utf8"));
|
|
71
|
+
remaining -= bounded.length;
|
|
72
|
+
}
|
|
73
|
+
if (metadata.size > room || remaining === 0)
|
|
74
|
+
truncated = true;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
for (const path of roots)
|
|
78
|
+
await visit(path, new Set());
|
|
79
|
+
return { entries, excerpt: excerpts.join(""), truncated };
|
|
80
|
+
}
|
package/dist/plugin/gate.js
CHANGED
|
@@ -1175,7 +1175,7 @@ export async function createWriteGate(project, value, toolDirectory = project.ro
|
|
|
1175
1175
|
const absolute = resolve(manifestPath.path);
|
|
1176
1176
|
if (await project.contains(absolute)) {
|
|
1177
1177
|
const normalized = await project.toRelativePath(absolute);
|
|
1178
|
-
if (!await isWritable(normalized))
|
|
1178
|
+
if (!await isWritable(normalized) && !await isExternalWritable(absolute))
|
|
1179
1179
|
throw new WriteDeniedError("manifest-scope", normalized);
|
|
1180
1180
|
return;
|
|
1181
1181
|
}
|
|
@@ -1240,21 +1240,16 @@ export async function createWriteGate(project, value, toolDirectory = project.ro
|
|
|
1240
1240
|
: undefined;
|
|
1241
1241
|
if (command !== undefined && declaredValidation.has(command))
|
|
1242
1242
|
return;
|
|
1243
|
+
// Shell syntax is not an execution permission boundary. Mission workers already use
|
|
1244
|
+
// native host permissions; only declared checks produce formal validation evidence.
|
|
1245
|
+
if (options?.investigativeShell && ["bash", "shell"].includes(_input.tool))
|
|
1246
|
+
return;
|
|
1243
1247
|
const extracted = extractWritePaths(_input.tool, output.args);
|
|
1244
1248
|
if (!extracted.applies)
|
|
1245
1249
|
return;
|
|
1246
|
-
// Mission workers may investigate with repository-specific executables without registering
|
|
1247
|
-
// every diagnostic as a formal check. Known write destinations/Git mutations still use the
|
|
1248
|
-
// same scope gate; this option never makes diagnostic output acceptance evidence.
|
|
1249
|
-
if (options?.investigativeShell && ["bash", "shell"].includes(_input.tool) &&
|
|
1250
|
-
extracted.issue?.cause === "executable-not-allowlisted" && extracted.paths.length === 0 &&
|
|
1251
|
-
!extracted.gitMutation && !extracted.remoteMutation)
|
|
1252
|
-
return;
|
|
1253
1250
|
if (extracted.ambiguous || (extracted.paths.length === 0 && !extracted.gitCommit)) {
|
|
1254
1251
|
if (extracted.issue !== undefined) {
|
|
1255
|
-
|
|
1256
|
-
["unsupported-curl-form", "unsupported-git-archive-form", "unsupported-webrequest-form"].includes(extracted.issue.cause);
|
|
1257
|
-
throw new WriteDeniedError("unclassified-command", issuePath(extracted.issue, formatCorrection));
|
|
1252
|
+
throw new WriteDeniedError("unclassified-command", issuePath(extracted.issue));
|
|
1258
1253
|
}
|
|
1259
1254
|
throw new WriteDeniedError("path-required", "<missing-path>");
|
|
1260
1255
|
}
|