intentdna 1.9.4 → 1.9.5
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/run.js +26 -2
- package/dist/cli/index.js +1 -1
- package/dist/compiler/compile.js +9 -0
- package/dist/hooks/cli.js +18 -0
- package/dist/hooks/enforce.js +8 -1
- package/dist/runtime/canonical-run-service.js +4 -1
- package/dist/runtime/claude-sdk.js +4 -2
- package/dist/runtime/codex-adapter.js +1 -1
- package/dist/runtime/execution-provider.d.ts +5 -0
- package/dist/runtime/execution-provider.js +8 -0
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/local-execution-authority.js +39 -4
- package/dist/runtime/providers/claude.d.ts +5 -0
- package/dist/runtime/providers/claude.js +61 -2
- package/dist/runtime/push-driver.d.ts +8 -1
- package/dist/runtime/push-driver.js +176 -5
- package/dist/runtime/result-store.js +69 -5
- package/dist/runtime/run-contracts.d.ts +34 -9
- package/dist/runtime/run-contracts.js +1 -1
- package/dist/runtime/worker-executor.js +14 -6
- package/dist/runtime/workspace-observation.d.ts +18 -3
- package/dist/runtime/workspace-observation.js +178 -20
- package/dist/schema/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "intentdna",
|
|
11
11
|
"description": "Declarative policy layer for AI agent behavior with plugin-managed hook runtime for Claude Code.",
|
|
12
|
-
"version": "1.9.
|
|
12
|
+
"version": "1.9.5",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "Samuel"
|
|
15
15
|
},
|
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
]
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
|
-
"version": "1.9.
|
|
28
|
+
"version": "1.9.5"
|
|
29
29
|
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Intent DNA - canonical dna run CLI transport. */
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { lstatSync } from "node:fs";
|
|
3
4
|
import { join, resolve } from "node:path";
|
|
4
5
|
import { toKebabCase } from "../../runtime/agent-md.js";
|
|
5
6
|
import { CanonicalRunApplication, } from "../../runtime/canonical-run-application.js";
|
|
@@ -301,9 +302,11 @@ function createProvider(binding) {
|
|
|
301
302
|
if (maxBudget === undefined || permissionMode === undefined) {
|
|
302
303
|
throw new Error("Claude run binding is missing launch budget or permission mode");
|
|
303
304
|
}
|
|
305
|
+
const mcpConfigPath = findProjectMcpConfig(binding.workspace_root);
|
|
304
306
|
return createClaudeExecutionProvider({
|
|
305
307
|
executable,
|
|
306
308
|
agentName: (packet) => `dna-${toKebabCase(packet.role)}`,
|
|
309
|
+
mcpConfigPath,
|
|
307
310
|
extraArgs: [
|
|
308
311
|
"--max-budget-usd",
|
|
309
312
|
String(maxBudget),
|
|
@@ -312,6 +315,22 @@ function createProvider(binding) {
|
|
|
312
315
|
],
|
|
313
316
|
});
|
|
314
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Resolve only regular, project-owned MCP config files. A missing config is
|
|
320
|
+
* valid for projects that do not use IntentDNA's MCP transport.
|
|
321
|
+
*/
|
|
322
|
+
function findProjectMcpConfig(projectRoot) {
|
|
323
|
+
const candidate = join(projectRoot, ".mcp.json");
|
|
324
|
+
try {
|
|
325
|
+
if (lstatSync(candidate).isFile())
|
|
326
|
+
return candidate;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// A missing or unreadable optional config is handled by Claude's normal
|
|
330
|
+
// project discovery; do not make provider construction fail.
|
|
331
|
+
}
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
315
334
|
function createDriver(runtime, view, metadata, projectDirectory) {
|
|
316
335
|
assertRunComposition(view, metadata, projectDirectory);
|
|
317
336
|
runtime.execution_authority.assertExecutionSupported();
|
|
@@ -386,6 +405,8 @@ function startMetadata(opts, projectDirectory) {
|
|
|
386
405
|
: { kind: "workflow", key: requireText(opts.workflowName, "--workflow") };
|
|
387
406
|
const taskId = requireText(opts.taskId, "--task");
|
|
388
407
|
const timeoutMs = opts.timeoutMs == null ? null : requireNonNegativeInteger(opts.timeoutMs, "--timeout-ms");
|
|
408
|
+
const provider = opts.provider ?? "claude";
|
|
409
|
+
const defaultPermissionMode = provider === "claude" ? "bypassPermissions" : "default";
|
|
389
410
|
return {
|
|
390
411
|
schema_version: "intentdna.canonical_run.v3",
|
|
391
412
|
project_directory: projectDirectory,
|
|
@@ -402,10 +423,13 @@ function startMetadata(opts, projectDirectory) {
|
|
|
402
423
|
},
|
|
403
424
|
agents_directory: opts.agentsDir ? resolve(projectDirectory, opts.agentsDir) : join(projectDirectory, ".claude", "agents"),
|
|
404
425
|
provider: {
|
|
405
|
-
kind:
|
|
426
|
+
kind: provider,
|
|
406
427
|
executable: opts.providerExecutable === undefined ? null : requireText(opts.providerExecutable, "--provider-executable"),
|
|
407
428
|
max_budget_usd: requirePositiveNumber(opts.maxBudget ?? 5, "--max-budget"),
|
|
408
|
-
|
|
429
|
+
// Durable workers have no interactive prompt available for tool approvals.
|
|
430
|
+
// Keep an explicitly supplied mode unchanged, but make the unattended
|
|
431
|
+
// default writable so required artifacts can be produced.
|
|
432
|
+
permission_mode: requireText(opts.permissionMode ?? defaultPermissionMode, "--permission-mode"),
|
|
409
433
|
},
|
|
410
434
|
max_concurrency: requirePositiveInteger(opts.maxConcurrency ?? 1, "--max-concurrency"),
|
|
411
435
|
timeout_ms: timeoutMs,
|
package/dist/cli/index.js
CHANGED
|
@@ -311,7 +311,7 @@ Options:
|
|
|
311
311
|
--timeout-ms <n> Per-attempt timeout in milliseconds
|
|
312
312
|
--cancellation-grace-ms <n> Process-tree cancellation grace period (default: 5000)
|
|
313
313
|
--max-budget <usd> Claude budget per Step attempt (default: 5)
|
|
314
|
-
--permission-mode <mode> Claude permission mode (default:
|
|
314
|
+
--permission-mode <mode> Claude permission mode (default: bypassPermissions for unattended runs)
|
|
315
315
|
--agents <dir> Project agent output directory (default: .claude/agents)
|
|
316
316
|
--reason <text> Cancellation or closure reason
|
|
317
317
|
--json Print lifecycle output as JSON
|
package/dist/compiler/compile.js
CHANGED
|
@@ -94,6 +94,12 @@ function geneToToolFilters(name, gene) {
|
|
|
94
94
|
/**
|
|
95
95
|
* Extract pre-execution gates from threshold and sense codons.
|
|
96
96
|
*/
|
|
97
|
+
function gateEnforcement(condition) {
|
|
98
|
+
return /^personal_data_exposure\s*==\s*false$/.test(condition)
|
|
99
|
+
|| /^tool_name\s+in\s+\[[^\]]+\]$/.test(condition)
|
|
100
|
+
? "runtime"
|
|
101
|
+
: "prompt_only";
|
|
102
|
+
}
|
|
97
103
|
function geneToGates(name, gene) {
|
|
98
104
|
if (gene.expression_level === 0)
|
|
99
105
|
return [];
|
|
@@ -105,6 +111,7 @@ function geneToGates(name, gene) {
|
|
|
105
111
|
action: "block",
|
|
106
112
|
message: `DNA threshold violation: ${codon.condition} (gene: ${name})`,
|
|
107
113
|
source_gene: name,
|
|
114
|
+
enforcement: gateEnforcement(codon.condition),
|
|
108
115
|
origin: codon.origin,
|
|
109
116
|
});
|
|
110
117
|
}
|
|
@@ -114,6 +121,7 @@ function geneToGates(name, gene) {
|
|
|
114
121
|
action: "escalate",
|
|
115
122
|
message: `DNA requires human approval for: ${codon.signal} (gene: ${name})`,
|
|
116
123
|
source_gene: name,
|
|
124
|
+
enforcement: "prompt_only",
|
|
117
125
|
});
|
|
118
126
|
}
|
|
119
127
|
if (codon.type === "sense" && codon.response === "block") {
|
|
@@ -122,6 +130,7 @@ function geneToGates(name, gene) {
|
|
|
122
130
|
action: "block",
|
|
123
131
|
message: `DNA blocks action on signal: ${codon.signal} (gene: ${name})`,
|
|
124
132
|
source_gene: name,
|
|
133
|
+
enforcement: "prompt_only",
|
|
125
134
|
});
|
|
126
135
|
}
|
|
127
136
|
}
|
package/dist/hooks/cli.js
CHANGED
|
@@ -625,6 +625,24 @@ export async function runHookEvent(options) {
|
|
|
625
625
|
await writeRuntimeDecision(mismatch, { output: mismatch, trace: { matched_rule: "workflow_runtime_revision", step_id: wfState?.current_step } }, wfState);
|
|
626
626
|
return finish(mismatch);
|
|
627
627
|
}
|
|
628
|
+
if (isCanonicalWorkflowState(wfState)) {
|
|
629
|
+
const stopOutput = silentOutput();
|
|
630
|
+
const traceId = randomUUID();
|
|
631
|
+
appendTrace(projectDir, {
|
|
632
|
+
trace_id: traceId,
|
|
633
|
+
event: "Stop",
|
|
634
|
+
workflow: wfState.workflow,
|
|
635
|
+
step: wfState.current_step,
|
|
636
|
+
decision: "allow",
|
|
637
|
+
duration_ms: 0,
|
|
638
|
+
timestamp: new Date().toISOString(),
|
|
639
|
+
}, sessionId).catch(() => { });
|
|
640
|
+
await writeRuntimeDecision(stopOutput, {
|
|
641
|
+
output: stopOutput,
|
|
642
|
+
trace: { matched_rule: "workflow_boundary", step_id: wfState.current_step },
|
|
643
|
+
}, wfState, traceId);
|
|
644
|
+
return finish(stopOutput);
|
|
645
|
+
}
|
|
628
646
|
let stopArtifactFacts = [];
|
|
629
647
|
if (wfState?.active) {
|
|
630
648
|
const artifactFacts = await finalizeAndResolveArtifactsForHook(projectDir, ir, wfState, event, sessionId);
|
package/dist/hooks/enforce.js
CHANGED
|
@@ -550,11 +550,18 @@ function enforceToolFilters(filters, input) {
|
|
|
550
550
|
}
|
|
551
551
|
function enforceGates(gates, input, strictBoundary = false) {
|
|
552
552
|
for (const gate of gates) {
|
|
553
|
+
if (gate.enforcement === "prompt_only")
|
|
554
|
+
continue;
|
|
553
555
|
const matches = evaluateGateCondition(gate.condition, input);
|
|
554
556
|
if (matches === null) {
|
|
557
|
+
// Older compiled IR did not declare enforcement. Provenance-backed
|
|
558
|
+
// thresholds and bare semantic signals were compiler-generated prompt
|
|
559
|
+
// constraints, so preserve that behavior during the schema transition.
|
|
560
|
+
if (gate.origin !== undefined || /^\w+$/.test(gate.condition))
|
|
561
|
+
continue;
|
|
555
562
|
if (!strictBoundary)
|
|
556
563
|
continue;
|
|
557
|
-
const detail = `
|
|
564
|
+
const detail = `Runtime gate condition '${gate.condition}' could not be evaluated.`;
|
|
558
565
|
if (gate.action === "block") {
|
|
559
566
|
return blockOutput(`[Intent DNA] gate_evaluation: ${detail}`);
|
|
560
567
|
}
|
|
@@ -107,6 +107,7 @@ async function observeAttemptWorkspace(workspaceRoot, workspaceId) {
|
|
|
107
107
|
kind: "attempt_workspace",
|
|
108
108
|
relative_root: ".",
|
|
109
109
|
excluded_relative_paths: observation.excluded_relative_paths,
|
|
110
|
+
opaque_external_dependencies: observation.opaque_external_dependencies,
|
|
110
111
|
},
|
|
111
112
|
observed_digest: observation.observed_digest,
|
|
112
113
|
entry_count: observation.entry_count,
|
|
@@ -691,7 +692,9 @@ class ContractVerifier {
|
|
|
691
692
|
}
|
|
692
693
|
const receipt = {
|
|
693
694
|
schema_version: CANONICAL_VERIFIER_EXECUTION_RECEIPT_SCHEMA_VERSION,
|
|
694
|
-
evidence_mode:
|
|
695
|
+
evidence_mode: (observation.scope.opaque_external_dependencies?.length ?? 0) === 0
|
|
696
|
+
? "historical_execution"
|
|
697
|
+
: "historical_execution_with_unbound_external_dependencies",
|
|
695
698
|
plan_id: request.plan.plan_id,
|
|
696
699
|
verifier_id: result.verifier_id,
|
|
697
700
|
verifier_definition_digest: digest(commandExecution.verifier),
|
|
@@ -51,7 +51,7 @@ export function compileForClaude(ir) {
|
|
|
51
51
|
}
|
|
52
52
|
// Add gate descriptions to prompt (so LLM is aware of constraints)
|
|
53
53
|
if (ir.pre_execution_gates.length > 0) {
|
|
54
|
-
promptLines.push("##
|
|
54
|
+
promptLines.push("## Policy Constraints");
|
|
55
55
|
for (const gate of ir.pre_execution_gates) {
|
|
56
56
|
promptLines.push(`- ${gate.message}`);
|
|
57
57
|
}
|
|
@@ -59,7 +59,9 @@ export function compileForClaude(ir) {
|
|
|
59
59
|
}
|
|
60
60
|
return {
|
|
61
61
|
system_prompt_addition: promptLines.join("\n"),
|
|
62
|
-
pre_tool_rules: ir.pre_execution_gates
|
|
62
|
+
pre_tool_rules: ir.pre_execution_gates
|
|
63
|
+
.filter((g) => g.enforcement !== "prompt_only")
|
|
64
|
+
.map((g) => ({
|
|
63
65
|
condition: g.condition,
|
|
64
66
|
action: g.action,
|
|
65
67
|
message: g.message,
|
|
@@ -988,7 +988,7 @@ function textMatchesCondition(text, condition) {
|
|
|
988
988
|
}
|
|
989
989
|
function matchingPreExecutionGate(ir, input) {
|
|
990
990
|
const subject = [input.tool_name, input.command, input.resource_ref, JSON.stringify(input.payload)].filter(Boolean).join(" ");
|
|
991
|
-
return ir.pre_execution_gates.find((gate) => textMatchesCondition(subject, gate.condition));
|
|
991
|
+
return ir.pre_execution_gates.find((gate) => gate.enforcement !== "prompt_only" && textMatchesCondition(subject, gate.condition));
|
|
992
992
|
}
|
|
993
993
|
function codexHookOutputForGate(gate) {
|
|
994
994
|
if (gate.action === "block") {
|
|
@@ -32,6 +32,11 @@ export interface WorkerExecution {
|
|
|
32
32
|
export declare class MalformedProviderResultError extends Error {
|
|
33
33
|
constructor(message: string);
|
|
34
34
|
}
|
|
35
|
+
export type ProviderTerminalReason = "hook_stopped" | "stop_hook_prevented";
|
|
36
|
+
export declare class ProviderTerminatedError extends Error {
|
|
37
|
+
readonly reason: ProviderTerminalReason;
|
|
38
|
+
constructor(reason: ProviderTerminalReason, message: string);
|
|
39
|
+
}
|
|
35
40
|
/**
|
|
36
41
|
* Decode the provider's final assistant text into the declared output contract.
|
|
37
42
|
* A single text output may use the assistant text directly. Other contracts use
|
|
@@ -4,6 +4,14 @@ export class MalformedProviderResultError extends Error {
|
|
|
4
4
|
this.name = "MalformedProviderResultError";
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
+
export class ProviderTerminatedError extends Error {
|
|
8
|
+
reason;
|
|
9
|
+
constructor(reason, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "ProviderTerminatedError";
|
|
12
|
+
this.reason = reason;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
7
15
|
function isRecord(value) {
|
|
8
16
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
17
|
}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -25,8 +25,8 @@ export { CanonicalAttemptOutcomeValidationError, canonicalAttemptOutcomeStatus,
|
|
|
25
25
|
export type { CanonicalAttemptOutcomeStatus, ValidatedCanonicalAttemptOutcome, } from "./canonical-attempt-outcome.js";
|
|
26
26
|
export { HandoffResolutionError, HandoffResolver, } from "./handoff-resolver.js";
|
|
27
27
|
export type { ReferenceHandoffRequest, QuoteHandoffRequest, StructuredHandoffRequest, NoHandoffRequest, HandoffRequest, StructuredValueValidator, HandoffResolverOptions, HandoffResolutionErrorCode, } from "./handoff-resolver.js";
|
|
28
|
-
export { MalformedProviderResultError, decodeDeclaredOutputs, } from "./execution-provider.js";
|
|
29
|
-
export type { ProviderLaunchSpec, ProviderParseContext, ParsedProviderResult, ExecutionProvider, WorkerExecution, } from "./execution-provider.js";
|
|
28
|
+
export { MalformedProviderResultError, ProviderTerminatedError, decodeDeclaredOutputs, } from "./execution-provider.js";
|
|
29
|
+
export type { ProviderLaunchSpec, ProviderParseContext, ParsedProviderResult, ExecutionProvider, ProviderTerminalReason, WorkerExecution, } from "./execution-provider.js";
|
|
30
30
|
export { createClaudeExecutionProvider } from "./providers/claude.js";
|
|
31
31
|
export type { ClaudeExecutionProviderOptions, } from "./providers/claude.js";
|
|
32
32
|
export { createCodexExecutionProvider } from "./providers/codex.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -13,7 +13,7 @@ export { RUN_STORE_SCHEMA_VERSION, LEGACY_RUN_STORE_SCHEMA_VERSION, CANONICAL_RU
|
|
|
13
13
|
export { ResultStoreError, ImmutableResultStore, ImmutableCanonicalResultStore, } from "./result-store.js";
|
|
14
14
|
export { CanonicalAttemptOutcomeValidationError, canonicalAttemptOutcomeStatus, validateCanonicalAttemptOutcome, } from "./canonical-attempt-outcome.js";
|
|
15
15
|
export { HandoffResolutionError, HandoffResolver, } from "./handoff-resolver.js";
|
|
16
|
-
export { MalformedProviderResultError, decodeDeclaredOutputs, } from "./execution-provider.js";
|
|
16
|
+
export { MalformedProviderResultError, ProviderTerminatedError, decodeDeclaredOutputs, } from "./execution-provider.js";
|
|
17
17
|
export { createClaudeExecutionProvider } from "./providers/claude.js";
|
|
18
18
|
export { createCodexExecutionProvider } from "./providers/codex.js";
|
|
19
19
|
export { runProcessTree, runProcessTreeWithTimeout, } from "./process-tree.js";
|
|
@@ -662,6 +662,20 @@ function nonlaunchSourceEvidenceDigest(record) {
|
|
|
662
662
|
});
|
|
663
663
|
}
|
|
664
664
|
function alreadyStoppedSourceEvidenceDigest(record) {
|
|
665
|
+
if (record.stop_receipt !== null) {
|
|
666
|
+
return digest({
|
|
667
|
+
schema_version: "intentdna.local_already_stopped_source_evidence.v2",
|
|
668
|
+
identity_digest: record.identity_digest,
|
|
669
|
+
execution_instance_id: record.execution_instance_id,
|
|
670
|
+
authority_instance_id: record.authority_instance_id,
|
|
671
|
+
backend: record.backend,
|
|
672
|
+
supervisor: record.supervisor,
|
|
673
|
+
provider: record.provider,
|
|
674
|
+
raw_completion: record.raw_completion,
|
|
675
|
+
normalized_process_outcome: record.normalized_process_outcome,
|
|
676
|
+
stop_receipt_digest: record.stop_receipt.receipt_digest,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
665
679
|
return digest({
|
|
666
680
|
schema_version: "intentdna.local_already_stopped_source_evidence.v1",
|
|
667
681
|
identity_digest: record.identity_digest,
|
|
@@ -674,6 +688,17 @@ function alreadyStoppedSourceEvidenceDigest(record) {
|
|
|
674
688
|
normalized_process_outcome: record.normalized_process_outcome,
|
|
675
689
|
});
|
|
676
690
|
}
|
|
691
|
+
function alreadyStoppedObservedOutcome(record) {
|
|
692
|
+
if (record.stop_receipt !== null)
|
|
693
|
+
return record.stop_receipt.payload.terminal_outcome;
|
|
694
|
+
if (record.normalized_process_outcome !== null) {
|
|
695
|
+
return terminalOutcomeToCanonical(record.normalized_process_outcome);
|
|
696
|
+
}
|
|
697
|
+
if (record.raw_completion !== null) {
|
|
698
|
+
return terminalOutcomeToCanonical(rawCompletionToOutcome(record.raw_completion));
|
|
699
|
+
}
|
|
700
|
+
throw new LocalExecutionAuthorityError("corrupt_state", "already-stopped evidence has no trusted completion outcome");
|
|
701
|
+
}
|
|
677
702
|
function stopReceiptSourceEvidenceDigest(record) {
|
|
678
703
|
return digest({
|
|
679
704
|
identity_digest: record.identity_digest,
|
|
@@ -868,7 +893,7 @@ function parseRecord(value) {
|
|
|
868
893
|
|| record.already_stopped_evidence.payload.started_at !== record.identity.started_at
|
|
869
894
|
|| record.already_stopped_evidence.payload.completed_at !== record.raw_completion.completed_at
|
|
870
895
|
|| canonicalizeJson(record.already_stopped_evidence.payload.observed_outcome)
|
|
871
|
-
!== canonicalizeJson(
|
|
896
|
+
!== canonicalizeJson(alreadyStoppedObservedOutcome(record))
|
|
872
897
|
|| record.already_stopped_evidence.payload.process_tree_drained !== true
|
|
873
898
|
|| !isCanonicalDigest(record.already_stopped_evidence.payload.stop_idempotency_key)
|
|
874
899
|
|| !isCanonicalTimestamp(record.already_stopped_evidence.payload.stop_requested_at)
|
|
@@ -2698,6 +2723,9 @@ export class LocalAttemptExecutionAuthority {
|
|
|
2698
2723
|
async issueReceipt(path, requestedOutcome, stoppedAt, expectedReceipt) {
|
|
2699
2724
|
return this.withLock(path, async () => {
|
|
2700
2725
|
const current = this.readRecord(path);
|
|
2726
|
+
if (current.already_stopped_evidence !== null) {
|
|
2727
|
+
throw new LocalExecutionAuthorityError("identity_conflict", "already-stopped evidence already owns the durable terminal transition");
|
|
2728
|
+
}
|
|
2701
2729
|
if (current.stop_receipt !== null) {
|
|
2702
2730
|
if (canonicalizeJson(current.stop_receipt.payload.terminal_outcome) !== canonicalizeJson(requestedOutcome)
|
|
2703
2731
|
|| current.stop_receipt.payload.stopped_at !== stoppedAt
|
|
@@ -2766,13 +2794,14 @@ export class LocalAttemptExecutionAuthority {
|
|
|
2766
2794
|
if (request.expected_receipt !== null) {
|
|
2767
2795
|
throw new LocalExecutionAuthorityError("identity_conflict", "expected stop receipt cannot match a naturally completed execution");
|
|
2768
2796
|
}
|
|
2769
|
-
if (current.
|
|
2797
|
+
if (current.stop_fact?.kind === "canonical_stop") {
|
|
2770
2798
|
throw new LocalExecutionAuthorityError("identity_conflict", "canonical stop already owns the durable terminal transition");
|
|
2771
2799
|
}
|
|
2772
2800
|
if (current.raw_completion === null) {
|
|
2773
2801
|
throw new LocalExecutionAuthorityError("unsupported", "trusted natural completion evidence is unavailable");
|
|
2774
2802
|
}
|
|
2775
|
-
const fencingEpoch =
|
|
2803
|
+
const fencingEpoch = current.stop_receipt?.payload.fencing_epoch
|
|
2804
|
+
?? await this.nextFencingEpoch();
|
|
2776
2805
|
const evidence = createWorkerAlreadyStoppedEvidence({
|
|
2777
2806
|
schema_version: "intentdna.worker_already_stopped_evidence.v1",
|
|
2778
2807
|
run_id: current.identity.run_id,
|
|
@@ -2787,7 +2816,7 @@ export class LocalAttemptExecutionAuthority {
|
|
|
2787
2816
|
fencing_epoch: fencingEpoch,
|
|
2788
2817
|
started_at: current.identity.started_at,
|
|
2789
2818
|
completed_at: current.raw_completion.completed_at,
|
|
2790
|
-
observed_outcome:
|
|
2819
|
+
observed_outcome: alreadyStoppedObservedOutcome(current),
|
|
2791
2820
|
process_tree_drained: true,
|
|
2792
2821
|
stop_idempotency_key: request.stop_idempotency_key,
|
|
2793
2822
|
stop_requested_at: request.requested_at,
|
|
@@ -2795,6 +2824,8 @@ export class LocalAttemptExecutionAuthority {
|
|
|
2795
2824
|
stop_requested_outcome: request.requested_outcome,
|
|
2796
2825
|
source_evidence_digest: alreadyStoppedSourceEvidenceDigest(current),
|
|
2797
2826
|
});
|
|
2827
|
+
if (current.stop_receipt !== null)
|
|
2828
|
+
return evidence;
|
|
2798
2829
|
const updated = {
|
|
2799
2830
|
...current,
|
|
2800
2831
|
phase: "receipted",
|
|
@@ -2859,6 +2890,10 @@ export class LocalAttemptExecutionAuthority {
|
|
|
2859
2890
|
return { kind: "already_stopped", drain_evidence: evidence };
|
|
2860
2891
|
}
|
|
2861
2892
|
if (record.stop_receipt !== null) {
|
|
2893
|
+
if (record.stop_fact?.kind !== "canonical_stop") {
|
|
2894
|
+
const evidence = await this.issueAlreadyStoppedEvidence(path, request);
|
|
2895
|
+
return { kind: "already_stopped", drain_evidence: evidence };
|
|
2896
|
+
}
|
|
2862
2897
|
const authority = await this.issueReceipt(path, request.requested_outcome, record.stop_receipt.payload.stopped_at, request.expected_receipt);
|
|
2863
2898
|
return { kind: "stopped_by_request", authority };
|
|
2864
2899
|
}
|
|
@@ -3,6 +3,11 @@ import type { StepPacket } from "../run-contracts.js";
|
|
|
3
3
|
export interface ClaudeExecutionProviderOptions {
|
|
4
4
|
readonly executable?: string;
|
|
5
5
|
readonly agentName?: (packet: StepPacket) => string;
|
|
6
|
+
/**
|
|
7
|
+
* Explicit project MCP configuration to load for each standalone attempt.
|
|
8
|
+
* A function keeps the launch spec bound to the packet's exact workspace.
|
|
9
|
+
*/
|
|
10
|
+
readonly mcpConfigPath?: string | ((packet: StepPacket) => string | null | undefined);
|
|
6
11
|
readonly extraArgs?: readonly string[];
|
|
7
12
|
readonly env?: NodeJS.ProcessEnv;
|
|
8
13
|
}
|
|
@@ -1,11 +1,57 @@
|
|
|
1
|
-
import { decodeDeclaredOutputs, MalformedProviderResultError, } from "../execution-provider.js";
|
|
1
|
+
import { decodeDeclaredOutputs, MalformedProviderResultError, ProviderTerminatedError, } from "../execution-provider.js";
|
|
2
2
|
function isRecord(value) {
|
|
3
3
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
4
|
}
|
|
5
|
+
const CLAUDE_HOOK_TERMINAL_REASONS = new Set([
|
|
6
|
+
"hook_stopped",
|
|
7
|
+
"stop_hook_prevented",
|
|
8
|
+
]);
|
|
9
|
+
const CLAUDE_DIAGNOSTIC_TOOL_NAMES = new Set([
|
|
10
|
+
"Bash",
|
|
11
|
+
"Edit",
|
|
12
|
+
"Glob",
|
|
13
|
+
"Grep",
|
|
14
|
+
"NotebookEdit",
|
|
15
|
+
"Read",
|
|
16
|
+
"Task",
|
|
17
|
+
"WebFetch",
|
|
18
|
+
"WebSearch",
|
|
19
|
+
"Write",
|
|
20
|
+
]);
|
|
21
|
+
function summarizePermissionDenials(value) {
|
|
22
|
+
if (!Array.isArray(value))
|
|
23
|
+
return null;
|
|
24
|
+
const toolNames = [...new Set(value.flatMap((denial) => (isRecord(denial)
|
|
25
|
+
&& typeof denial.tool_name === "string"
|
|
26
|
+
&& CLAUDE_DIAGNOSTIC_TOOL_NAMES.has(denial.tool_name)
|
|
27
|
+
? [denial.tool_name]
|
|
28
|
+
: [])))].slice(0, 3);
|
|
29
|
+
const count = value.length > 999 ? "999+" : String(value.length);
|
|
30
|
+
return toolNames.length > 0
|
|
31
|
+
? `permission_denials=${count}; denied_tools=${toolNames.join(",")}`
|
|
32
|
+
: `permission_denials=${count}`;
|
|
33
|
+
}
|
|
34
|
+
function standaloneClaudePrompt(packet) {
|
|
35
|
+
const workingDirectory = JSON.stringify(packet.workspace.working_directory);
|
|
36
|
+
return [
|
|
37
|
+
"IntentDNA runtime workspace contract:",
|
|
38
|
+
`- The exact, authoritative process working directory for this attempt is ${workingDirectory}.`,
|
|
39
|
+
"- Use paths relative to that working directory for repository-local file access whenever possible.",
|
|
40
|
+
"- Never infer, reconstruct, rename, or guess an absolute repository path from a session name, project name, transcript location, or encoded directory name.",
|
|
41
|
+
`- If an absolute repository path is required, copy ${workingDirectory} exactly instead of reconstructing it.`,
|
|
42
|
+
"- This is a standalone attempt. Do not search for or read Claude Code session transcripts, including files under ~/.claude/projects, to recover task context.",
|
|
43
|
+
"",
|
|
44
|
+
"Task:",
|
|
45
|
+
packet.standalone_prompt,
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
5
48
|
export function createClaudeExecutionProvider(options = {}) {
|
|
6
49
|
return {
|
|
7
50
|
name: "claude",
|
|
8
51
|
createLaunch(packet) {
|
|
52
|
+
const mcpConfigPath = typeof options.mcpConfigPath === "function"
|
|
53
|
+
? options.mcpConfigPath(packet)
|
|
54
|
+
: options.mcpConfigPath;
|
|
9
55
|
return {
|
|
10
56
|
command: options.executable ?? "claude",
|
|
11
57
|
args: [
|
|
@@ -17,9 +63,12 @@ export function createClaudeExecutionProvider(options = {}) {
|
|
|
17
63
|
"--session-id",
|
|
18
64
|
packet.worker_session_id,
|
|
19
65
|
"--no-session-persistence",
|
|
66
|
+
...(mcpConfigPath === undefined || mcpConfigPath === null
|
|
67
|
+
? []
|
|
68
|
+
: ["--mcp-config", mcpConfigPath]),
|
|
20
69
|
...(options.extraArgs ?? []),
|
|
21
70
|
],
|
|
22
|
-
stdin: packet
|
|
71
|
+
stdin: standaloneClaudePrompt(packet),
|
|
23
72
|
cwd: packet.workspace.working_directory,
|
|
24
73
|
...(options.env ? { env: options.env } : {}),
|
|
25
74
|
};
|
|
@@ -35,6 +84,16 @@ export function createClaudeExecutionProvider(options = {}) {
|
|
|
35
84
|
if (!isRecord(value)) {
|
|
36
85
|
throw new MalformedProviderResultError("Claude output must be a JSON object");
|
|
37
86
|
}
|
|
87
|
+
const hookTerminalReason = typeof value.terminal_reason === "string"
|
|
88
|
+
&& CLAUDE_HOOK_TERMINAL_REASONS.has(value.terminal_reason)
|
|
89
|
+
? value.terminal_reason
|
|
90
|
+
: null;
|
|
91
|
+
if (hookTerminalReason !== null) {
|
|
92
|
+
const denials = summarizePermissionDenials(value.permission_denials);
|
|
93
|
+
throw new ProviderTerminatedError(hookTerminalReason, denials
|
|
94
|
+
? `Claude terminal_reason=${hookTerminalReason}; ${denials}`
|
|
95
|
+
: `Claude terminal_reason=${hookTerminalReason}`);
|
|
96
|
+
}
|
|
38
97
|
if (value.is_error === true) {
|
|
39
98
|
throw new MalformedProviderResultError(typeof value.result === "string"
|
|
40
99
|
? value.result
|
|
@@ -4,7 +4,7 @@ import type { ExecutionProvider } from "./execution-provider.js";
|
|
|
4
4
|
import type { CanonicalAttemptOutcome, CanonicalEvidenceRef, CanonicalResultDigest, RunId, StepOutput, StepPacket, TerminalOutcome, WorkerSessionId } from "./run-contracts.js";
|
|
5
5
|
import { type RunBindingPayload } from "./run-binding.js";
|
|
6
6
|
import { executeWorkerAttempt, type WorkerExecutionAuthorityBinding, type WorkerExecutionWithEvents, type WorkerExecutorOptions } from "./worker-executor.js";
|
|
7
|
-
export type PushDriverErrorCode = "invalid_configuration" | "invalid_service_response" | "invalid_execution_result" | "invalid_provider_output" | "unsupported_output_contract" | "unsupported" | "attempt_relaunch" | "attach_indeterminate" | "authority_preparation_abandon_failed" | "driver_failure_settlement_failed" | "wait_stopped";
|
|
7
|
+
export type PushDriverErrorCode = "invalid_configuration" | "invalid_service_response" | "invalid_execution_result" | "invalid_provider_output" | "unsupported_output_contract" | "unsupported" | "attempt_relaunch" | "attach_indeterminate" | "submission_indeterminate" | "authority_preparation_abandon_failed" | "driver_failure_settlement_failed" | "wait_stopped";
|
|
8
8
|
export declare class PushDriverError extends Error {
|
|
9
9
|
readonly code: PushDriverErrorCode;
|
|
10
10
|
constructor(code: PushDriverErrorCode, message: string, options?: ErrorOptions);
|
|
@@ -13,6 +13,10 @@ export declare class PushDriverIndeterminateAttachError extends PushDriverError
|
|
|
13
13
|
readonly disposition: "retain_preparation";
|
|
14
14
|
constructor(attemptId: string, cause: unknown);
|
|
15
15
|
}
|
|
16
|
+
export declare class PushDriverIndeterminateSubmissionError extends PushDriverError {
|
|
17
|
+
readonly disposition: "retain_attempt";
|
|
18
|
+
constructor(attemptId: string, cause: unknown);
|
|
19
|
+
}
|
|
16
20
|
export interface PushDriverService {
|
|
17
21
|
inspect(runId: RunId): Promise<RunView>;
|
|
18
22
|
resume(runId: RunId): Promise<RunView>;
|
|
@@ -175,6 +179,9 @@ export declare class PushDriver {
|
|
|
175
179
|
private attemptError;
|
|
176
180
|
private assertHeartbeatLease;
|
|
177
181
|
private startHeartbeat;
|
|
182
|
+
private submissionIsCommitted;
|
|
183
|
+
private submissionIsDefinitivelyFenced;
|
|
184
|
+
private reconcileSubmission;
|
|
178
185
|
private attachIsDefinitivelyFenced;
|
|
179
186
|
private abandonPreparation;
|
|
180
187
|
private reconcilePreparedAttach;
|