@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.155
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/CHANGELOG.md +23 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +273 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +114 -16
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +214 -24
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -7
- package/dist/services/retained-native-hook.js +245 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +12 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +209 -43
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +8 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +252 -66
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -145,10 +145,11 @@ export class FrozenGenericExecution {
|
|
|
145
145
|
this.#flow.stage(id);
|
|
146
146
|
return { profile: this.profile(this.settings.stage_profiles[id]), session_mode: this.settings.stage_session_mode };
|
|
147
147
|
}
|
|
148
|
-
work(stage, profileId) {
|
|
148
|
+
work(stage, profileId, workerStage = stage) {
|
|
149
149
|
const coordinator = this.stage(stage).profile;
|
|
150
|
-
const
|
|
151
|
-
|
|
150
|
+
const roleProfile = this.stage(workerStage).profile;
|
|
151
|
+
const id = profileId ?? roleProfile.id;
|
|
152
|
+
if (id !== roleProfile.id && id !== coordinator.id && !this.settings.work_profiles.includes(id))
|
|
152
153
|
fail("Work profile is not approved for this RUN");
|
|
153
154
|
const worker = this.profile(id);
|
|
154
155
|
if (this.settings.delegation.mode === "native" && !sameSessionProfile(coordinator, worker))
|
|
@@ -11,7 +11,6 @@ import { readOperationProgress, readOperationRecord, readOperationWaiting, suspe
|
|
|
11
11
|
import { recoverDaemonReply } from "../harness-runtime/lib/daemon-observation.mjs";
|
|
12
12
|
import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
|
|
13
13
|
import { readBoundedReleaseFile } from "./release-source.js";
|
|
14
|
-
const maxAdapterOutputBytes = 1_000_000;
|
|
15
14
|
/** Native hooks need a real executable, not a PATH-dependent shell expression. */
|
|
16
15
|
export function prepareHarnessFlowExecutable(context, stateDir, engine) {
|
|
17
16
|
// Native login shells rewrite search paths. The owned SDK must execute with
|
|
@@ -132,7 +131,6 @@ function runAdapterTransport(input, stateDir, binding = {}) {
|
|
|
132
131
|
const child = spawn(input.executable, input.args, { env, ...(input.cwd ? { cwd: input.cwd } : {}), detached, stdio: ["ignore", "pipe", "pipe"] });
|
|
133
132
|
let stdout = "";
|
|
134
133
|
let stderr = "";
|
|
135
|
-
let outputTooLarge = false;
|
|
136
134
|
let timedOut = false;
|
|
137
135
|
let aborted = false;
|
|
138
136
|
let cleanup;
|
|
@@ -159,7 +157,7 @@ function runAdapterTransport(input, stateDir, binding = {}) {
|
|
|
159
157
|
reject(callbackError);
|
|
160
158
|
}
|
|
161
159
|
else
|
|
162
|
-
reject(new AppError(
|
|
160
|
+
reject(new AppError(timedOut ? "harness_adapter_timeout" : aborted ? "harness_adapter_aborted" : "harness_adapter_cleanup_unconfirmed", "Adapter transport cleanup could not be confirmed", 1, details));
|
|
163
161
|
});
|
|
164
162
|
};
|
|
165
163
|
const deliver = (callback) => {
|
|
@@ -220,14 +218,6 @@ function runAdapterTransport(input, stateDir, binding = {}) {
|
|
|
220
218
|
else
|
|
221
219
|
input.signal?.addEventListener("abort", abort, { once: true });
|
|
222
220
|
const evidence = (value) => input.onEvidence?.({ operation_id: input.operationId, executable: input.executable, args: input.args, ...value, at: new Date().toISOString() });
|
|
223
|
-
const append = (current, chunk) => {
|
|
224
|
-
if (Buffer.byteLength(current) + Buffer.byteLength(chunk) > maxAdapterOutputBytes) {
|
|
225
|
-
outputTooLarge = true;
|
|
226
|
-
stopTransport();
|
|
227
|
-
return current;
|
|
228
|
-
}
|
|
229
|
-
return current + chunk;
|
|
230
|
-
};
|
|
231
221
|
const observeDiagnostic = (line) => {
|
|
232
222
|
if (prettyDiagnostic === null) {
|
|
233
223
|
if (line.trimEnd() === "}")
|
|
@@ -281,7 +271,7 @@ function runAdapterTransport(input, stateDir, binding = {}) {
|
|
|
281
271
|
}
|
|
282
272
|
}
|
|
283
273
|
};
|
|
284
|
-
child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout
|
|
274
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => { stdout += chunk; });
|
|
285
275
|
// Diagnostics have no lifetime quota. Stream to the existing evidence sink
|
|
286
276
|
// and carry only a bounded tail in the disposable RPC receipt.
|
|
287
277
|
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
@@ -322,8 +312,6 @@ function runAdapterTransport(input, stateDir, binding = {}) {
|
|
|
322
312
|
deliver(() => evidence({ status, signal, timed_out: timedOut, aborted, stdout, stderr, ...(spawnError ? { error: spawnError.message } : {}) }));
|
|
323
313
|
if (spawnError)
|
|
324
314
|
return reject(new AppError("harness_adapter_failed", "Harness adapter call failed", 1, { cause: adapterCause(spawnError.message), ...(callbackError ? { evidence_error: String(callbackError) } : {}) }));
|
|
325
|
-
if (outputTooLarge)
|
|
326
|
-
return reject(new AppError("harness_adapter_output_limit", "Harness adapter exceeded the bounded reply limit", 1, { executable: input.executable, args: input.args.slice(0, 3), max_output_bytes: maxAdapterOutputBytes }));
|
|
327
315
|
if (timedOut)
|
|
328
316
|
return reject(Object.assign(new AppError("harness_adapter_timeout", "Harness adapter call timed out", 1, { executable: input.executable, args: input.args.slice(0, 3), timeout_ms: timeoutMs }), { observationClock: clock }));
|
|
329
317
|
if (aborted)
|
|
@@ -143,7 +143,7 @@ export function memoryRepairAssignedHookCwd(context, input) {
|
|
|
143
143
|
export function memoryNativeReviewAssignedHookCwd(context, input) {
|
|
144
144
|
const work = context.db.get("SELECT status,payload_json FROM works WHERE project_id=? AND run_id=? AND work_id=?", [input.projectId, input.runId, input.workId]);
|
|
145
145
|
const payload = JSON.parse(work?.payload_json ?? "null");
|
|
146
|
-
if (
|
|
146
|
+
if (!["memory-review", "interactive-review"].includes(payload?.kind) || payload?.read_only !== true)
|
|
147
147
|
return null;
|
|
148
148
|
const ready = context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.native_review_copy_ready' AND json_extract(payload_json,'$.intent.run_id')=? AND json_extract(payload_json,'$.intent.work_id')=?", [input.projectId, input.runId, input.workId]);
|
|
149
149
|
if (!ready)
|
package/dist/services/hooks.js
CHANGED
|
@@ -319,7 +319,11 @@ export function handleCodexHook(context, input) {
|
|
|
319
319
|
const lifecycle = lifecycleFacts(command);
|
|
320
320
|
if (!lifecycle && !continuationCommand(command))
|
|
321
321
|
return { ok: true, observed: false, reason: "event_not_participating", event: eventName };
|
|
322
|
-
const parentProviderSessionId = rawChildProviderSessionId
|
|
322
|
+
const parentProviderSessionId = rawChildProviderSessionId
|
|
323
|
+
? context.codexHookProof?.sessionId === rawChildProviderSessionId
|
|
324
|
+
? context.codexHookProof.parentSessionId
|
|
325
|
+
: codexNativeParentId(payload, rawChildProviderSessionId)
|
|
326
|
+
: null;
|
|
323
327
|
// Resume legitimately receives an answer through stdin. It is matched by
|
|
324
328
|
// immutable lifecycle arguments below; never reject or rewrite that pipe.
|
|
325
329
|
const bootstrapStageStart = lifecycle?.bootstrapStageStart;
|
|
@@ -415,7 +419,18 @@ export function handleCodexHook(context, input) {
|
|
|
415
419
|
const toolCallId = stringValue(payload.tool_use_id) ?? stringValue(payload.event_id) ?? stringValue(payload.delivery_id) ?? stringValue(payload.id);
|
|
416
420
|
if (!daemonId || !rootProviderSessionId || !providerSessionId || !toolCallId)
|
|
417
421
|
throw new AppError("codex_identity_missing", "Managed Codex lifecycle hook has no complete native invocation identity", 1, { invocation_id: invocationId });
|
|
418
|
-
|
|
422
|
+
let executionCwd;
|
|
423
|
+
try {
|
|
424
|
+
executionCwd = assignedLifecycleCwd(context, project.root, lifecycle, providerSessionId, command, stringValue(payload.cwd), { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId }, "codex-desktop");
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
// A proven external Work is not its Stage coordinator. Deny the tool
|
|
428
|
+
// normally; a policy refusal is not a broken native hook or a receipt.
|
|
429
|
+
if (!(error instanceof AppError) || error.code !== "work_stage_boundary")
|
|
430
|
+
throw error;
|
|
431
|
+
return { ...common, observed: false, reason: error.code, hookSpecificOutput: { hookEventName: "PreToolUse",
|
|
432
|
+
permissionDecision: "deny", permissionDecisionReason: error.message } };
|
|
433
|
+
}
|
|
419
434
|
const receipt = observeLifecycleInvocation(context, {
|
|
420
435
|
currentEventKey: eventKey,
|
|
421
436
|
...(invocationId ? { id: invocationId } : {}),
|
|
@@ -741,6 +756,12 @@ function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, co
|
|
|
741
756
|
if ((!facts && !auxiliary) || facts?.analysis.supported === false)
|
|
742
757
|
return undefined;
|
|
743
758
|
const proven = identity && harness ? managedNativeInvocationScope(context, projectRoot, identity, harness) : null;
|
|
759
|
+
if (proven && (facts?.invocation.operation.startsWith("stage_") || facts?.invocation.operation.startsWith("delivery_")) && !facts.bootstrapStageStart
|
|
760
|
+
&& context.db.get("SELECT 1 FROM sqlite_master WHERE type='table' AND name='work_launches'")) {
|
|
761
|
+
const worker = context.db.get("SELECT w.work_id FROM works w JOIN work_launches l ON l.work_id=w.work_id JOIN work_sessions ws ON ws.work_id=w.work_id JOIN sessions s ON s.session_id=ws.session_id AND s.project_id=w.project_id JOIN projects p ON p.id=w.project_id WHERE p.root=? AND w.run_id=? AND s.harness=? AND s.provider_session_id=? AND l.status IN ('launching','start_requested') LIMIT 1", [proven.projectRoot, proven.runId, harness, providerSessionId]);
|
|
762
|
+
if (worker)
|
|
763
|
+
throw new AppError("work_stage_boundary", "This external Work cannot issue coordinator Stage or delivery commands. Complete only its assigned Work; after successful work finish, end this Turn. The coordinator continues the Stage.", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
764
|
+
}
|
|
744
765
|
const constraint = proven ?? (identity ? { projectRoot, daemonId: identity.daemonId, rootSessionId: identity.rootSessionId } : undefined);
|
|
745
766
|
const issuedRunId = facts && !facts.runId && !facts.workId && !facts.bootstrapStageStart && constraint ? observedLifecycleInvocation(context, command, constraint, identity ? { identity } : undefined)?.scope.runId : null;
|
|
746
767
|
if (facts?.invocation.operation.startsWith("stage_") && !facts.bootstrapStageStart && !facts.runId && !issuedRunId)
|
|
@@ -885,7 +906,10 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
885
906
|
// Syntax refusal precedes assignment/cwd lookup: an unsupported AST cannot
|
|
886
907
|
// supply a route or execution authority, including a short public Stage call.
|
|
887
908
|
const shellRefusal = lifecycle?.analysis.supported === false;
|
|
888
|
-
const
|
|
909
|
+
const proof = context.nativeHookProof;
|
|
910
|
+
if (proof && (proof.harness !== harness || proof.daemonId !== daemonId || proof.sessionId !== providerSessionId || proof.toolCallId !== toolCallId))
|
|
911
|
+
throw new AppError("native_hook_unproven", "Verified native Work callback differs from its physical identity", 1);
|
|
912
|
+
const nativeIdentity = { daemonId, rootSessionId: proof?.rootSessionId ?? nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId };
|
|
889
913
|
const provenScope = lifecycle && !shellRefusal ? managedNativeInvocationScope(context, expectedRoot, nativeIdentity, harness) : null;
|
|
890
914
|
const issuedRunId = lifecycle && !lifecycle.runId && !lifecycle.workId && !lifecycle.bootstrapStageStart
|
|
891
915
|
? observedLifecycleInvocation(context, command, provenScope ?? { projectRoot: expectedRoot, daemonId, rootSessionId: nativeIdentity.rootSessionId }, { identity: nativeIdentity })?.scope.runId : null;
|
|
@@ -939,7 +963,7 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
939
963
|
let inserted = false;
|
|
940
964
|
const receipt = observeLifecycleInvocation(context, {
|
|
941
965
|
currentEventKey: baseKey,
|
|
942
|
-
command, harness, projectId: project.id, ...(!shellRefusal && workspaceBound ? { executionCwd: expectedCwd } : {}), identity:
|
|
966
|
+
command, harness, projectId: project.id, ...(!shellRefusal && workspaceBound ? { executionCwd: expectedCwd } : {}), identity: nativeIdentity,
|
|
943
967
|
recordReceipt: scope => { inserted = persist(scope); return baseKey; }
|
|
944
968
|
});
|
|
945
969
|
const admitted = { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
|
|
@@ -1066,6 +1090,7 @@ function lifecycleFacts(command) {
|
|
|
1066
1090
|
const workStart = invocation.operation === "work_start";
|
|
1067
1091
|
const workFinish = invocation.operation === "work_finish";
|
|
1068
1092
|
const workFail = invocation.operation === "work_fail";
|
|
1093
|
+
const delivery = invocation.operation.startsWith("delivery_");
|
|
1069
1094
|
return {
|
|
1070
1095
|
analysis,
|
|
1071
1096
|
invocation,
|
|
@@ -1076,8 +1101,8 @@ function lifecycleFacts(command) {
|
|
|
1076
1101
|
workStart, workFinish, workFail,
|
|
1077
1102
|
bootstrapStageStart: stageStart && commandHasOption(invocation, "bootstrap"),
|
|
1078
1103
|
commandProjectRoot: commandOption(invocation, "project-root"),
|
|
1079
|
-
stageName: stageStart || stageFinish || stagePause || stageResume ? commandOption(invocation, "stage") : undefined,
|
|
1080
|
-
runId: stageStart || stageFinish || stagePause || stageResume ? commandPosition(invocation, 0) : undefined,
|
|
1104
|
+
stageName: stageStart || stageFinish || stagePause || stageResume || delivery ? commandOption(invocation, "stage") : undefined,
|
|
1105
|
+
runId: stageStart || stageFinish || stagePause || stageResume || delivery ? commandPosition(invocation, 0) : undefined,
|
|
1081
1106
|
workId: stagePause || stageResume ? commandOption(invocation, "work") : workStart || workFinish || workFail ? commandPosition(invocation, 0) : undefined,
|
|
1082
1107
|
contextSha256: commandOption(invocation, "context-sha256")
|
|
1083
1108
|
};
|
|
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
4
4
|
import { Ajv } from "ajv/dist/ajv.js";
|
|
5
|
+
import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
|
|
6
|
+
import { isDeliveryFlow } from "../domain/delivery-flows.js";
|
|
5
7
|
/** Static E1 contracts only. No ingress, dispatch or runtime defaults are changed. */
|
|
6
8
|
export const hybridContractNames = [
|
|
7
9
|
"release-manifest", "harness-catalog", "model-catalog", "discovery-result",
|
|
@@ -249,7 +251,7 @@ export function validateHybridContract(name, input, context = {}) {
|
|
|
249
251
|
fail("foreign_bundle_canon", "/canon");
|
|
250
252
|
if (hybridDigest(data.project_pack) !== hybridDigest(context.bundleProvenance.project_pack))
|
|
251
253
|
fail("foreign_bundle_project_pack", "/project_pack");
|
|
252
|
-
if ((data.flow_id === "mb-
|
|
254
|
+
if ((isSourceSnapshotFlow(data.flow_id) || data.flow_id === "mb-fix" || isDeliveryFlow(data.flow_id)) !== (data.project_pack === null))
|
|
253
255
|
fail("invalid_bundle_project_pack_role", "/project_pack");
|
|
254
256
|
const memoryFlow = data.flow_id === "mb-init" || data.flow_id === "mb-upgrade";
|
|
255
257
|
if (memoryFlow && context.bootstrapIdentity?.projectRoot !== context.bundleProjectRoot)
|
|
@@ -289,6 +291,9 @@ export function validateHybridContract(name, input, context = {}) {
|
|
|
289
291
|
}
|
|
290
292
|
};
|
|
291
293
|
const descriptor = loaded(data.descriptor), settings = loaded(data.execution_settings), profiles = loaded(data.profile_settings);
|
|
294
|
+
if (data.flow_id === "interactive" && (!descriptor || record(descriptor).origin !== "project"
|
|
295
|
+
|| record(data.project_pack).source_commit !== record(data.canon).commit))
|
|
296
|
+
fail("foreign_project_flow_provenance", "/project_pack");
|
|
292
297
|
// Execution receipts are evidence references, not just trusted facts. They
|
|
293
298
|
// must survive reopening the RUN inside the same frozen blob inventory.
|
|
294
299
|
for (const reference of [record(settings).attached_session_receipt,
|
|
@@ -8,7 +8,7 @@ import { hybridDigest, validateHybridShape } from "./hybrid-contracts.js";
|
|
|
8
8
|
import { admitBundledFlow } from "./stage-handlers.js";
|
|
9
9
|
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
10
10
|
const limit = 1024 * 1024;
|
|
11
|
-
const flowIds = ["mb-init", "mb-upgrade", "mb-audit", "project-review"];
|
|
11
|
+
const flowIds = ["mb-init", "mb-upgrade", "mb-audit", "project-review", "mb-distill", "mb-fix", "review-fix", "release", "deploy", "publish"];
|
|
12
12
|
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
13
13
|
function refuse(reason) {
|
|
14
14
|
throw new AppError("installed_flow_source_invalid", "Installed flow source is not the selected verified canon", 1, { reason, phase: "prepare", effect: "no_effect" });
|
|
@@ -117,6 +117,41 @@ export class PreparedInstalledFlowSource {
|
|
|
117
117
|
JSON.parse(content.toString("utf8"));
|
|
118
118
|
blobs.set(workSchema, Buffer.from(content));
|
|
119
119
|
}
|
|
120
|
+
if (flowId === "mb-fix" || flowId === "review-fix") {
|
|
121
|
+
for (const logical of [`dd-flow/${flowId}/hybrid/index.md`, "mbb/frontmatter-standards.md", "mbb/cross-references.md", "mbb/named-deferrals-guide.md"]) {
|
|
122
|
+
const content = readBoundedReleaseFile(containedFile(memoryRoot, logical), limit), name = `support/${logical}`;
|
|
123
|
+
if (!content.length || (total += content.length) > 32 * limit || folded.has(name.toLowerCase()))
|
|
124
|
+
refuse("invalid_support_inventory");
|
|
125
|
+
new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
126
|
+
folded.set(name.toLowerCase(), name);
|
|
127
|
+
blobs.set(name, Buffer.from(content));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (flowId === "mb-distill") {
|
|
131
|
+
// Both lenses need their actual comparison baseline, not only titles.
|
|
132
|
+
const directories = ["mbb", "mbb/aspects", "mbb/templates", "dd-flow/common", "dd-flow/vnext",
|
|
133
|
+
"dd-flow/plan", "dd-flow/code", "dd-flow/merge", "dd-flow/workers", "dd-flow/def",
|
|
134
|
+
"dd-flow/mb-distill/aspects", "dd-flow/mb-distill/process-aspects"];
|
|
135
|
+
const support = [...directories.flatMap(logical => {
|
|
136
|
+
const source = containedFile(memoryRoot, logical);
|
|
137
|
+
directory(source);
|
|
138
|
+
return fs.readdirSync(source).filter(name => name.endsWith(".md")).sort().map(name => `${logical}/${name}`);
|
|
139
|
+
}), "dd-flow/mb-audit.md", "dd-flow/mb-upgrade/index.md"];
|
|
140
|
+
for (const logical of support) {
|
|
141
|
+
const content = readBoundedReleaseFile(containedFile(memoryRoot, logical), limit), name = `support/${logical}`;
|
|
142
|
+
if (!content.length || (total += content.length) > 32 * limit || folded.has(name.toLowerCase()))
|
|
143
|
+
refuse("invalid_support_inventory");
|
|
144
|
+
new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
145
|
+
folded.set(name.toLowerCase(), name);
|
|
146
|
+
blobs.set(name, Buffer.from(content));
|
|
147
|
+
}
|
|
148
|
+
const workSchema = "schemas/distill-work-result.schema.json";
|
|
149
|
+
const content = readBoundedReleaseFile(containedFile(flowRoot, workSchema), limit);
|
|
150
|
+
if (!content.length || (total += content.length) > 32 * limit || folded.has(workSchema.toLowerCase()))
|
|
151
|
+
refuse("invalid_support_inventory");
|
|
152
|
+
JSON.parse(content.toString("utf8"));
|
|
153
|
+
blobs.set(workSchema, Buffer.from(content));
|
|
154
|
+
}
|
|
120
155
|
// Verify compressed/expanded archive identity again after collecting bytes.
|
|
121
156
|
// A coherent descriptor+prompt edit cannot become a new trusted selection.
|
|
122
157
|
const final = readSetupStatus(home);
|
|
@@ -167,7 +202,7 @@ export class PreparedInstalledFlowSource {
|
|
|
167
202
|
|| typeof value.canon.version !== "string" || !/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(value.canon.version)
|
|
168
203
|
|| !value.descriptor || Object.keys(value.descriptor).sort().join() !== "path,sha256"
|
|
169
204
|
|| !flowIds.some(id => value.descriptor.path === `${id}/flow-descriptor.json`) || typeof value.descriptor.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.descriptor.sha256)
|
|
170
|
-
|| !Array.isArray(value.blobs) || value.blobs.length < 1 || value.blobs.length >
|
|
205
|
+
|| !Array.isArray(value.blobs) || value.blobs.length < 1 || value.blobs.length > 512)
|
|
171
206
|
refuse("invalid_retained_source");
|
|
172
207
|
const blobs = new Map(), folded = new Set();
|
|
173
208
|
let total = 0;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { parse } from "shell-quote";
|
|
5
|
+
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import { appendAudit } from "./audit.js";
|
|
7
|
+
import { runtimeHarness } from "./execution-policy.js";
|
|
8
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
9
|
+
import { hookSessionIdentity } from "./hooks.js";
|
|
10
|
+
import { prepareOperatorProfile } from "./operator-profiles.js";
|
|
11
|
+
import { findProjectByRoot } from "./projects.js";
|
|
12
|
+
import { parseCommandArgs } from "./lifecycle-command.js";
|
|
13
|
+
const eventType = "interactive.attached_session";
|
|
14
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
15
|
+
function refuse(reason) {
|
|
16
|
+
throw new AppError("interactive_attached_session_required", "Interactive needs its current trusted attached Session; choose managed execution explicitly otherwise", 2, { phase: "prepare", effect: "no_effect", reason, next_action: "provide_matching_attached_session_or_choose_managed" });
|
|
17
|
+
}
|
|
18
|
+
function checkedScope(scope) {
|
|
19
|
+
if (!scope || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(scope.requestId)
|
|
20
|
+
|| [scope.projectRoot, scope.workspace].some(value => typeof value !== "string" || !path.isAbsolute(value)))
|
|
21
|
+
refuse("invalid_scope");
|
|
22
|
+
try {
|
|
23
|
+
if ([scope.projectRoot, scope.workspace].some(value => fs.realpathSync(value) !== value))
|
|
24
|
+
refuse("noncanonical_scope");
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
refuse("workspace_unavailable");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function project(context, scope) {
|
|
31
|
+
const value = findProjectByRoot(context, scope.projectRoot);
|
|
32
|
+
if (!value)
|
|
33
|
+
refuse("registered_project_required");
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function launchArguments(context, command) {
|
|
37
|
+
let dynamic = false;
|
|
38
|
+
const entries = parse(command, () => { dynamic = true; return ""; });
|
|
39
|
+
if (dynamic || entries.some(entry => typeof entry !== "string") || /[`\n\r]/.test(command))
|
|
40
|
+
refuse("standalone_launch_required");
|
|
41
|
+
const words = entries;
|
|
42
|
+
let index = 0;
|
|
43
|
+
if (words[0]?.startsWith("DD_FLOW_HOME=")) {
|
|
44
|
+
if (words[0].slice("DD_FLOW_HOME=".length) !== context.ddFlowHome)
|
|
45
|
+
refuse("launch_home_mismatch");
|
|
46
|
+
index++;
|
|
47
|
+
}
|
|
48
|
+
if (path.basename(words[index] ?? "") === "node" && path.basename(words[index + 1] ?? "") === "cli.js")
|
|
49
|
+
index++;
|
|
50
|
+
else if (path.basename(words[index] ?? "") !== "dd-flow")
|
|
51
|
+
refuse("standalone_launch_required");
|
|
52
|
+
return words.slice(index + 1);
|
|
53
|
+
}
|
|
54
|
+
function checkLaunchCommand(context, scope, command) {
|
|
55
|
+
const args = parseCommandArgs(launchArguments(context, command)), option = (key) => args.options.get(key);
|
|
56
|
+
if (args.positional.join(" ") !== "flow interactive" || option("help") || option("execution")?.some(value => value !== "attached")
|
|
57
|
+
|| option("request-id")?.length !== 1 || option("request-id")[0] !== scope.requestId
|
|
58
|
+
|| option("project-root")?.length !== 1 || option("project-root")[0] !== scope.projectRoot
|
|
59
|
+
|| (option("workspace") ? option("workspace").length !== 1 || option("workspace")[0] !== scope.workspace : scope.workspace !== scope.projectRoot))
|
|
60
|
+
refuse("launch_scope_mismatch");
|
|
61
|
+
}
|
|
62
|
+
function binding(context, scope, selected, source, fresh) {
|
|
63
|
+
if (!source || typeof source.eventKey !== "string" || !source.eventKey || typeof source.command !== "string"
|
|
64
|
+
|| !source.command.trim() || Buffer.byteLength(source.command) > 32 * 1024)
|
|
65
|
+
refuse("exact_launch_receipt_required");
|
|
66
|
+
checkLaunchCommand(context, scope, source.command);
|
|
67
|
+
const profile = prepareOperatorProfile(selected), selectedProject = project(context, scope);
|
|
68
|
+
const event = context.db.get("SELECT id,event_name,status,sanitized_summary,created_at FROM hook_events WHERE project_id=? AND event_key=?", [selectedProject.id, source.eventKey]);
|
|
69
|
+
if (!event || event.event_name !== "PreToolUse" || event.status !== (fresh ? "observed" : "claimed"))
|
|
70
|
+
refuse("unavailable_launch_receipt");
|
|
71
|
+
const age = Date.parse(context.now()) - Date.parse(event.created_at);
|
|
72
|
+
// Reuse the existing hook admission horizon; historical restore does not
|
|
73
|
+
// pretend that the original launch event proves a currently live Session.
|
|
74
|
+
if (fresh && (!Number.isFinite(age) || age < 0 || age > 60_000))
|
|
75
|
+
refuse("stale_launch_receipt");
|
|
76
|
+
let summary;
|
|
77
|
+
try {
|
|
78
|
+
summary = JSON.parse(event.sanitized_summary);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return refuse("exact_launch_receipt_required");
|
|
82
|
+
}
|
|
83
|
+
if ((summary?.command ?? summary?.tool_input?.command) !== source.command)
|
|
84
|
+
refuse("launch_command_mismatch");
|
|
85
|
+
const identity = hookSessionIdentity(context, selectedProject.id, source.eventKey);
|
|
86
|
+
if (identity.parentSessionId || !identity.providerSessionId || identity.harness !== runtimeHarness(profile))
|
|
87
|
+
refuse("root_session_mismatch");
|
|
88
|
+
if (identity.cwd !== scope.workspace)
|
|
89
|
+
refuse("session_workspace_mismatch");
|
|
90
|
+
if (identity.provider !== profile.provider || identity.model !== profile.model || identity.reasoning !== profile.reasoning
|
|
91
|
+
|| identity.mode !== profile.mode)
|
|
92
|
+
refuse("session_profile_mismatch");
|
|
93
|
+
return { schema_id: "dd-flow/interactive-attached-session@1", scope: structuredClone(scope), profile,
|
|
94
|
+
hook_event_id: event.id, event_key: source.eventKey, command: source.command, harness: identity.harness,
|
|
95
|
+
provider_session_id: identity.providerSessionId, session_id: identity.sessionId, daemon_id: identity.daemonId, observed_at: event.created_at };
|
|
96
|
+
}
|
|
97
|
+
function retained(context, scope) {
|
|
98
|
+
const selectedProject = project(context, scope);
|
|
99
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.binding.scope.requestId')=? LIMIT 2", [selectedProject.id, eventType, scope.requestId]);
|
|
100
|
+
if (rows.length > 1)
|
|
101
|
+
refuse("ambiguous_attached_binding");
|
|
102
|
+
if (!rows.length)
|
|
103
|
+
return null;
|
|
104
|
+
if (Buffer.byteLength(rows[0].payload_json) > 256 * 1024)
|
|
105
|
+
refuse("invalid_attached_binding");
|
|
106
|
+
let value;
|
|
107
|
+
try {
|
|
108
|
+
value = JSON.parse(rows[0].payload_json);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return refuse("invalid_attached_binding");
|
|
112
|
+
}
|
|
113
|
+
if (!value || !value.binding || !value.binding.scope || Object.keys(value).sort().join() !== "binding,sha256" || hybridDigest(value.binding.scope) !== hybridDigest(scope)
|
|
114
|
+
|| hash(Buffer.from(JSON.stringify(value.binding))) !== value.sha256)
|
|
115
|
+
refuse("attached_binding_changed");
|
|
116
|
+
const actual = binding(context, scope, value.binding.profile, { eventKey: value.binding.event_key, command: value.binding.command }, false);
|
|
117
|
+
if (hybridDigest(actual) !== hybridDigest(value.binding))
|
|
118
|
+
refuse("attached_receipt_changed");
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
/** Produces generic execution receipt facts from the trusted hook ledger, not
|
|
122
|
+
* model-authored receipt JSON or ambient Session/model environment variables.
|
|
123
|
+
* This does not create a RUN or prove independent review/delegation capacity. */
|
|
124
|
+
export class PreparedInteractiveAttachedSession {
|
|
125
|
+
sha256;
|
|
126
|
+
#binding;
|
|
127
|
+
constructor(value) {
|
|
128
|
+
this.#binding = structuredClone(value);
|
|
129
|
+
this.sha256 = hash(Buffer.from(JSON.stringify(value)));
|
|
130
|
+
Object.freeze(this);
|
|
131
|
+
}
|
|
132
|
+
static prepare(context, scope, profile, source) {
|
|
133
|
+
checkedScope(scope);
|
|
134
|
+
if (retained(context, scope))
|
|
135
|
+
refuse("restore_retained_attached_binding");
|
|
136
|
+
return new PreparedInteractiveAttachedSession(binding(context, scope, profile, source, true));
|
|
137
|
+
}
|
|
138
|
+
/** Discover only an exact current argv observation. Same request/root/profile
|
|
139
|
+
* with a different intake or selector is not evidence for this invocation. */
|
|
140
|
+
static prepareCurrent(context, scope, profile, args) {
|
|
141
|
+
checkedScope(scope);
|
|
142
|
+
if (!Array.isArray(args) || args.length > 1024 || args.some(arg => typeof arg !== "string" || arg.includes("\0"))
|
|
143
|
+
|| args[0] !== "flow" || args[1] !== "interactive")
|
|
144
|
+
refuse("exact_launch_receipt_required");
|
|
145
|
+
if (retained(context, scope))
|
|
146
|
+
refuse("restore_retained_attached_binding");
|
|
147
|
+
const selectedProject = project(context, scope), matches = [];
|
|
148
|
+
const rows = context.db.all("SELECT event_key,sanitized_summary FROM hook_events WHERE project_id=? AND event_name='PreToolUse' AND status='observed' AND created_at>=? ORDER BY id DESC LIMIT 129", [selectedProject.id, new Date(Date.parse(context.now()) - 60_000).toISOString()]);
|
|
149
|
+
if (rows.length > 128)
|
|
150
|
+
refuse("launch_observation_window_exceeds_bound");
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
if (typeof row.sanitized_summary !== "string" || Buffer.byteLength(row.sanitized_summary) > 64 * 1024)
|
|
153
|
+
continue;
|
|
154
|
+
let summary;
|
|
155
|
+
try {
|
|
156
|
+
summary = JSON.parse(row.sanitized_summary);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const command = summary?.command ?? summary?.tool_input?.command;
|
|
162
|
+
if (typeof command !== "string" || Buffer.byteLength(command) > 32 * 1024)
|
|
163
|
+
continue;
|
|
164
|
+
try {
|
|
165
|
+
if (hybridDigest(launchArguments(context, command)) !== hybridDigest(args))
|
|
166
|
+
continue;
|
|
167
|
+
matches.push(binding(context, scope, profile, { eventKey: row.event_key, command }, true));
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (!(error instanceof AppError) || error.code !== "interactive_attached_session_required")
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (matches.length !== 1)
|
|
175
|
+
refuse(matches.length ? "ambiguous_launch_receipt" : "unavailable_launch_receipt");
|
|
176
|
+
return new PreparedInteractiveAttachedSession(matches[0]);
|
|
177
|
+
}
|
|
178
|
+
static restore(context, scope) {
|
|
179
|
+
checkedScope(scope);
|
|
180
|
+
const value = retained(context, scope);
|
|
181
|
+
if (!value)
|
|
182
|
+
refuse("attached_binding_missing");
|
|
183
|
+
return new PreparedInteractiveAttachedSession(value.binding);
|
|
184
|
+
}
|
|
185
|
+
snapshot() { return structuredClone(this.#binding); }
|
|
186
|
+
evidence() {
|
|
187
|
+
const value = this.#binding, bytes = Buffer.from(JSON.stringify(value));
|
|
188
|
+
const reference = { path: "receipts/interactive-attached-session.json", sha256: this.sha256 };
|
|
189
|
+
const receipts = new Map([[this.sha256, { kind: "attached-session-binding", ...value.scope,
|
|
190
|
+
harness: value.profile.harness, profileRevision: hybridDigest(value.profile) }]]);
|
|
191
|
+
return { reference, bytes, receipts };
|
|
192
|
+
}
|
|
193
|
+
/** Pair hook claim and custody with the caller's request/RUN writer. */
|
|
194
|
+
retainPreparedSql(context) {
|
|
195
|
+
if (!context.db.isOwnedWriteTransaction?.())
|
|
196
|
+
refuse("owned_writer_required");
|
|
197
|
+
const value = this.#binding, prior = retained(context, value.scope);
|
|
198
|
+
if (prior) {
|
|
199
|
+
if (prior.sha256 !== this.sha256)
|
|
200
|
+
refuse("attached_binding_changed");
|
|
201
|
+
return { changed: false };
|
|
202
|
+
}
|
|
203
|
+
const current = binding(context, value.scope, value.profile, { eventKey: value.event_key, command: value.command }, true);
|
|
204
|
+
if (hybridDigest(current) !== hybridDigest(value))
|
|
205
|
+
refuse("attached_receipt_changed");
|
|
206
|
+
const selectedProject = project(context, value.scope);
|
|
207
|
+
const claimed = context.db.run("UPDATE hook_events SET status='claimed',claimed_at=? WHERE id=? AND project_id=? AND status='observed'", [context.now(), value.hook_event_id, selectedProject.id]);
|
|
208
|
+
if (claimed.changes !== 1)
|
|
209
|
+
refuse("launch_receipt_consumed");
|
|
210
|
+
appendAudit(context, { projectId: selectedProject.id, eventType, payload: { binding: value, sha256: this.sha256 } });
|
|
211
|
+
if (retained(context, value.scope)?.sha256 !== this.sha256)
|
|
212
|
+
refuse("attached_readback_failed");
|
|
213
|
+
return { changed: true };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { PreparedInteractiveGitBaseline } from "./interactive-git-baseline.js";
|
|
3
|
+
import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
4
|
+
import { restoreInteractiveBeforeVersions } from "./interactive-before-versions.js";
|
|
5
|
+
import { readInteractiveBundleData } from "./interactive-bundle-data.js";
|
|
6
|
+
const logical = "runtime/git-baseline.json";
|
|
7
|
+
function refuse(reason) {
|
|
8
|
+
throw new AppError("interactive_baseline_custody_invalid", "Interactive requires its own retained entry baseline", 2, { reason, effect: "no_effect" });
|
|
9
|
+
}
|
|
10
|
+
function selected(context, scope, bundle, file = logical) {
|
|
11
|
+
if (bundle.manifest.flow_id !== "interactive" || bundle.manifest.run_id !== scope.runId)
|
|
12
|
+
refuse("interactive_bundle_required");
|
|
13
|
+
assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
14
|
+
return readInteractiveBundleData(bundle, file);
|
|
15
|
+
}
|
|
16
|
+
/** Initial physical observation, not RUN allocation or mutation authority. The
|
|
17
|
+
* factory must revalidate before retaining these bytes and starting work. */
|
|
18
|
+
export function prepareInteractiveGitBaseline(input) {
|
|
19
|
+
const baseline = PreparedInteractiveGitBaseline.capture(input);
|
|
20
|
+
return { path: logical, sha256: baseline.sha256, bytes: baseline.bytes(), baseline };
|
|
21
|
+
}
|
|
22
|
+
/** SQL/in-memory fence for Stage acceptance: no project/Git/filesystem lookup,
|
|
23
|
+
* profile selection, fresh capture or adoption of a caller's replacement JSON. */
|
|
24
|
+
export function assertInteractiveGitBaselineCustody(context, scope, bundle, expectedSha256) {
|
|
25
|
+
if (selected(context, scope, bundle).sha256 !== expectedSha256)
|
|
26
|
+
refuse("baseline_binding_changed");
|
|
27
|
+
}
|
|
28
|
+
/** Historical custody uses the frozen execution scope. Current physical scope
|
|
29
|
+
* and supported relocation must be admitted separately before subsequent work. */
|
|
30
|
+
export function readPreparedInteractiveGitBaseline(context, scope, bundle) {
|
|
31
|
+
return context.db.readSnapshot(() => {
|
|
32
|
+
const { bytes, reference, sha256 } = selected(context, scope, bundle);
|
|
33
|
+
const baseline = PreparedInteractiveGitBaseline.restore(bytes, sha256), value = baseline.snapshot();
|
|
34
|
+
const frozen = bundle.execution.snapshot().scope;
|
|
35
|
+
if (value.project_root !== frozen.projectRoot || value.workspace !== frozen.workspace)
|
|
36
|
+
refuse("baseline_scope_changed");
|
|
37
|
+
return { baseline, sha256,
|
|
38
|
+
reference: { root: "run", path: `execution-bundle/${logical}`, sha256: reference.sha256 } };
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function assertInteractiveBeforeVersionsCustody(context, scope, bundle, expectedSha256) {
|
|
42
|
+
if (selected(context, scope, bundle, "runtime/before-versions.json").sha256 !== expectedSha256)
|
|
43
|
+
refuse("before_versions_binding_changed");
|
|
44
|
+
}
|
|
45
|
+
/** Initial before-images selected from this same RUN/baseline. Later per-turn
|
|
46
|
+
* before-images require their own accepted Work/packet custody, not bundle edits. */
|
|
47
|
+
export function readPreparedInteractiveBeforeVersions(context, scope, bundle) {
|
|
48
|
+
return context.db.readSnapshot(() => {
|
|
49
|
+
const baseline = readPreparedInteractiveGitBaseline(context, scope, bundle);
|
|
50
|
+
const { bytes, reference, sha256 } = selected(context, scope, bundle, "runtime/before-versions.json");
|
|
51
|
+
return { value: restoreInteractiveBeforeVersions(bytes, sha256, baseline.baseline), sha256,
|
|
52
|
+
reference: { root: "run", path: "execution-bundle/runtime/before-versions.json", sha256: reference.sha256 } };
|
|
53
|
+
});
|
|
54
|
+
}
|