intentdna 1.7.0 → 1.7.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/README.md +26 -15
- package/dist/cli/commands/feedback.d.ts +2 -2
- package/dist/cli/commands/feedback.js +5 -5
- package/dist/cli/commands/sync.js +8 -5
- package/dist/cli/commands/verify.d.ts +32 -0
- package/dist/cli/commands/verify.js +173 -6
- package/dist/cli/index.js +21 -9
- package/dist/compiler/activate.js +11 -6
- package/dist/compiler/cascade.d.ts +3 -1
- package/dist/compiler/cascade.js +23 -1
- package/dist/compiler/compile.js +1 -0
- package/dist/compiler/index.d.ts +5 -1
- package/dist/compiler/index.js +7 -4
- package/dist/compiler/input-resolver.d.ts +8 -1
- package/dist/compiler/input-resolver.js +128 -22
- package/dist/compiler/provenance.d.ts +8 -0
- package/dist/compiler/provenance.js +127 -0
- package/dist/governance/index.d.ts +4 -3
- package/dist/governance/index.js +3 -4
- package/dist/governance/runtime-decision-event.d.ts +85 -0
- package/dist/governance/runtime-decision-event.js +231 -0
- package/dist/governance/types.d.ts +3 -3
- package/dist/governance/types.js +3 -3
- package/dist/hooks/cli.d.ts +1 -0
- package/dist/hooks/cli.js +181 -20
- package/dist/hooks/state.d.ts +3 -10
- package/dist/hooks/state.js +147 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/tools-observability.js +3 -3
- package/dist/mcp/tools-state.d.ts +1 -1
- package/dist/mcp/tools-state.js +17 -7
- package/dist/report/kernel-report.d.ts +27 -0
- package/dist/report/kernel-report.js +60 -19
- package/dist/report/kernel-signals.d.ts +5 -2
- package/dist/report/kernel-signals.js +84 -2
- package/dist/report/report-package.d.ts +9 -1
- package/dist/report/report-package.js +32 -27
- package/dist/runtime/claude-sdk.d.ts +9 -4
- package/dist/runtime/claude-sdk.js +9 -0
- package/dist/runtime/plugin-adapter.d.ts +5 -1
- package/dist/runtime/plugin-adapter.js +2 -0
- package/dist/schema/types.d.ts +51 -0
- package/package.json +1 -1
- package/spec/README.md +1 -1
- package/spec/schema-spec.md +78 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
export const RUNTIME_DECISION_EVENT_SCHEMA_VERSION = "intentdna.runtime_decision_event.v1.7.1";
|
|
2
|
+
export const CASCADE_LAYER_NAMES = [
|
|
3
|
+
"species",
|
|
4
|
+
"enterprise",
|
|
5
|
+
"project",
|
|
6
|
+
"personal",
|
|
7
|
+
"role",
|
|
8
|
+
"context",
|
|
9
|
+
"task",
|
|
10
|
+
];
|
|
11
|
+
const RUNTIME_DECISIONS = new Set(["allow", "warn", "block", "escalate", "validate"]);
|
|
12
|
+
const ENFORCEMENT_POINTS = new Set([
|
|
13
|
+
"hook",
|
|
14
|
+
"sdk",
|
|
15
|
+
"ci",
|
|
16
|
+
"plugin_runtime",
|
|
17
|
+
"mcp_external_write",
|
|
18
|
+
"unknown",
|
|
19
|
+
"unsupported",
|
|
20
|
+
]);
|
|
21
|
+
const CASCADE_LAYER_VALUES = new Set([...CASCADE_LAYER_NAMES, "unknown", "unsupported"]);
|
|
22
|
+
const CODON_TYPES = new Set(["attract", "repel", "threshold", "weight", "sense"]);
|
|
23
|
+
const EVIDENCE_REF_TYPES = new Set([
|
|
24
|
+
"trace",
|
|
25
|
+
"audit",
|
|
26
|
+
"artifact",
|
|
27
|
+
"verifier",
|
|
28
|
+
"handoff",
|
|
29
|
+
"legacy",
|
|
30
|
+
"unknown",
|
|
31
|
+
"unsupported",
|
|
32
|
+
]);
|
|
33
|
+
const REQUIRED_STRING_FIELDS = [
|
|
34
|
+
"schema_version",
|
|
35
|
+
"event_id",
|
|
36
|
+
"timestamp",
|
|
37
|
+
"org_id",
|
|
38
|
+
"team_id",
|
|
39
|
+
"user_id",
|
|
40
|
+
"project_id",
|
|
41
|
+
"agent_id",
|
|
42
|
+
"agent_role",
|
|
43
|
+
"agent_type",
|
|
44
|
+
"session_agent",
|
|
45
|
+
"harness_adapter",
|
|
46
|
+
"runtime",
|
|
47
|
+
"policy_bundle_id",
|
|
48
|
+
"policy_bundle_version",
|
|
49
|
+
"compiled_ir_hash",
|
|
50
|
+
"decision_reason",
|
|
51
|
+
"enforcement_point",
|
|
52
|
+
"session_id",
|
|
53
|
+
"run_id",
|
|
54
|
+
"step_id",
|
|
55
|
+
"tool_name",
|
|
56
|
+
"action_kind",
|
|
57
|
+
"resource_ref",
|
|
58
|
+
];
|
|
59
|
+
const REQUIRED_WINNING_CONSTRAINT_FIELDS = [
|
|
60
|
+
"constraint_id",
|
|
61
|
+
"source_layer",
|
|
62
|
+
"source_dna_id",
|
|
63
|
+
"gene_id",
|
|
64
|
+
"codon_type",
|
|
65
|
+
"provenance_reason",
|
|
66
|
+
];
|
|
67
|
+
const REQUIRED_HARNESS_CONTEXT_FIELDS = [
|
|
68
|
+
"agent_id",
|
|
69
|
+
"agent_role",
|
|
70
|
+
"agent_type",
|
|
71
|
+
"session_agent",
|
|
72
|
+
"harness_adapter",
|
|
73
|
+
"runtime",
|
|
74
|
+
];
|
|
75
|
+
const ENTERPRISE_EVIDENCE_FIELDS = [
|
|
76
|
+
...REQUIRED_STRING_FIELDS,
|
|
77
|
+
"decision",
|
|
78
|
+
];
|
|
79
|
+
const ENTERPRISE_WINNING_CONSTRAINT_FIELDS = [
|
|
80
|
+
...REQUIRED_WINNING_CONSTRAINT_FIELDS,
|
|
81
|
+
"action",
|
|
82
|
+
];
|
|
83
|
+
function isRecord(value) {
|
|
84
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
85
|
+
}
|
|
86
|
+
function hasString(record, field) {
|
|
87
|
+
return typeof record[field] === "string" && record[field].trim().length > 0;
|
|
88
|
+
}
|
|
89
|
+
function isPlaceholder(value) {
|
|
90
|
+
return value === "unknown" || value === "unsupported";
|
|
91
|
+
}
|
|
92
|
+
function validateStringFields(record, fields, prefix, errors) {
|
|
93
|
+
for (const field of fields) {
|
|
94
|
+
if (!hasString(record, field))
|
|
95
|
+
errors.push(`${prefix}${field} must be a non-empty string`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function validateCascadeLayers(value, errors) {
|
|
99
|
+
if (!isRecord(value)) {
|
|
100
|
+
errors.push("cascade_layers must be an object with all cascade layer keys");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
for (const layer of CASCADE_LAYER_NAMES) {
|
|
104
|
+
const entry = value[layer];
|
|
105
|
+
if (!isRecord(entry)) {
|
|
106
|
+
errors.push(`cascade_layers.${layer} must be an object`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (entry.layer !== layer)
|
|
110
|
+
errors.push(`cascade_layers.${layer}.layer must be ${layer}`);
|
|
111
|
+
validateStringFields(entry, ["source_id", "source_ref", "fingerprint", "status"], `cascade_layers.${layer}.`, errors);
|
|
112
|
+
if (hasString(entry, "status") && entry.status !== "active" && entry.status !== "unknown" && entry.status !== "unsupported") {
|
|
113
|
+
errors.push(`cascade_layers.${layer}.status must be active, unknown, or unsupported`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function validateWinningConstraint(value, event, errors) {
|
|
118
|
+
if (!isRecord(value)) {
|
|
119
|
+
errors.push("winning_constraint must be an object");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
validateStringFields(value, REQUIRED_WINNING_CONSTRAINT_FIELDS, "winning_constraint.", errors);
|
|
123
|
+
if (hasString(value, "source_layer") && !CASCADE_LAYER_VALUES.has(value.source_layer)) {
|
|
124
|
+
errors.push("winning_constraint.source_layer must be a known cascade layer, unknown, or unsupported");
|
|
125
|
+
}
|
|
126
|
+
if (hasString(value, "codon_type") && !CODON_TYPES.has(value.codon_type)) {
|
|
127
|
+
errors.push("winning_constraint.codon_type must be a known codon type");
|
|
128
|
+
}
|
|
129
|
+
if (!RUNTIME_DECISIONS.has(value.action)) {
|
|
130
|
+
errors.push("winning_constraint.action must be allow, warn, block, escalate, or validate");
|
|
131
|
+
}
|
|
132
|
+
if (hasString(event, "decision") && value.action !== event.decision) {
|
|
133
|
+
errors.push("winning_constraint.action must match top-level decision");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function validateHarnessRuntimeContext(value, event, errors) {
|
|
137
|
+
if (!isRecord(value)) {
|
|
138
|
+
errors.push("harness_runtime_context must be an object");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
validateStringFields(value, REQUIRED_HARNESS_CONTEXT_FIELDS, "harness_runtime_context.", errors);
|
|
142
|
+
for (const field of REQUIRED_HARNESS_CONTEXT_FIELDS) {
|
|
143
|
+
if (hasString(event, field) && hasString(value, field) && event[field] !== value[field]) {
|
|
144
|
+
errors.push(`harness_runtime_context.${field} must match top-level ${field}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function validateEvidenceRefs(value, errors) {
|
|
149
|
+
if (!Array.isArray(value)) {
|
|
150
|
+
errors.push("evidence_refs must be an array");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (value.length === 0) {
|
|
154
|
+
errors.push("evidence_refs must contain at least one evidence reference");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
value.forEach((ref, index) => {
|
|
158
|
+
if (!isRecord(ref)) {
|
|
159
|
+
errors.push(`evidence_refs.${index} must be an object`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
validateStringFields(ref, ["type", "ref"], `evidence_refs.${index}.`, errors);
|
|
163
|
+
if (hasString(ref, "type") && !EVIDENCE_REF_TYPES.has(ref.type)) {
|
|
164
|
+
errors.push(`evidence_refs.${index}.type must be a known evidence reference type`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function hasCompleteEnterpriseProvenance(event) {
|
|
169
|
+
if (!ENTERPRISE_EVIDENCE_FIELDS.every((field) => hasString(event, field) && !isPlaceholder(event[field])))
|
|
170
|
+
return false;
|
|
171
|
+
if (!isRecord(event.harness_runtime_context))
|
|
172
|
+
return false;
|
|
173
|
+
if (!REQUIRED_HARNESS_CONTEXT_FIELDS.every((field) => hasString(event.harness_runtime_context, field) && !isPlaceholder(event.harness_runtime_context[field])))
|
|
174
|
+
return false;
|
|
175
|
+
if (!isRecord(event.winning_constraint))
|
|
176
|
+
return false;
|
|
177
|
+
if (!ENTERPRISE_WINNING_CONSTRAINT_FIELDS.every((field) => hasString(event.winning_constraint, field) && !isPlaceholder(event.winning_constraint[field])))
|
|
178
|
+
return false;
|
|
179
|
+
if (!isRecord(event.cascade_layers))
|
|
180
|
+
return false;
|
|
181
|
+
for (const layer of CASCADE_LAYER_NAMES) {
|
|
182
|
+
const entry = event.cascade_layers[layer];
|
|
183
|
+
if (!isRecord(entry))
|
|
184
|
+
return false;
|
|
185
|
+
if (entry.status !== "active")
|
|
186
|
+
return false;
|
|
187
|
+
if (["source_id", "source_ref", "fingerprint"].some((field) => !hasString(entry, field) || isPlaceholder(entry[field])))
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
if (!Array.isArray(event.evidence_refs) || event.evidence_refs.length === 0)
|
|
191
|
+
return false;
|
|
192
|
+
for (const ref of event.evidence_refs) {
|
|
193
|
+
if (!isRecord(ref))
|
|
194
|
+
return false;
|
|
195
|
+
if (!hasString(ref, "type") || isPlaceholder(ref.type) || ref.type === "legacy")
|
|
196
|
+
return false;
|
|
197
|
+
if (!hasString(ref, "ref") || isPlaceholder(ref.ref))
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
export function validateRuntimeDecisionEvent(event) {
|
|
203
|
+
const errors = [];
|
|
204
|
+
if (!isRecord(event)) {
|
|
205
|
+
return {
|
|
206
|
+
valid: false,
|
|
207
|
+
classification: "legacy_diagnostic",
|
|
208
|
+
errors: ["RuntimeDecisionEvent must be an object"],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
validateStringFields(event, REQUIRED_STRING_FIELDS, "", errors);
|
|
212
|
+
if (event.schema_version !== RUNTIME_DECISION_EVENT_SCHEMA_VERSION) {
|
|
213
|
+
errors.push(`schema_version must be ${RUNTIME_DECISION_EVENT_SCHEMA_VERSION}`);
|
|
214
|
+
}
|
|
215
|
+
if (!RUNTIME_DECISIONS.has(event.decision)) {
|
|
216
|
+
errors.push("decision must be allow, warn, block, escalate, or validate");
|
|
217
|
+
}
|
|
218
|
+
if (!ENFORCEMENT_POINTS.has(event.enforcement_point)) {
|
|
219
|
+
errors.push("enforcement_point must identify hook, sdk, ci, plugin runtime, MCP external write, unknown, or unsupported");
|
|
220
|
+
}
|
|
221
|
+
validateCascadeLayers(event.cascade_layers, errors);
|
|
222
|
+
validateWinningConstraint(event.winning_constraint, event, errors);
|
|
223
|
+
validateHarnessRuntimeContext(event.harness_runtime_context, event, errors);
|
|
224
|
+
validateEvidenceRefs(event.evidence_refs, errors);
|
|
225
|
+
const valid = errors.length === 0;
|
|
226
|
+
return {
|
|
227
|
+
valid,
|
|
228
|
+
classification: valid && hasCompleteEnterpriseProvenance(event) ? "enterprise_evidence" : "legacy_diagnostic",
|
|
229
|
+
errors,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Intent DNA — Remote Governance Types
|
|
3
3
|
*
|
|
4
|
-
* Architecture reservation for enterprise governance communication.
|
|
5
|
-
*
|
|
4
|
+
* Architecture reservation for future remote enterprise governance communication.
|
|
5
|
+
* Current Enterprise Safety Slice uses local/shared policy bundles and RuntimeDecisionEvent evidence.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Reserved capabilities:
|
|
8
8
|
* - Audit data reporting to central server
|
|
9
9
|
* - Enterprise DNA policy pull
|
|
10
10
|
* - Policy update push notifications
|
package/dist/governance/types.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Intent DNA — Remote Governance Types
|
|
3
3
|
*
|
|
4
|
-
* Architecture reservation for enterprise governance communication.
|
|
5
|
-
*
|
|
4
|
+
* Architecture reservation for future remote enterprise governance communication.
|
|
5
|
+
* Current Enterprise Safety Slice uses local/shared policy bundles and RuntimeDecisionEvent evidence.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Reserved capabilities:
|
|
8
8
|
* - Audit data reporting to central server
|
|
9
9
|
* - Enterprise DNA policy pull
|
|
10
10
|
* - Policy update push notifications
|
package/dist/hooks/cli.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export interface RunHookEventOptions {
|
|
|
47
47
|
sessionId?: string;
|
|
48
48
|
roles?: Record<string, RoleDef>;
|
|
49
49
|
}
|
|
50
|
+
export declare function normalizeHookOutputForEvent(output: HookOutput, event: HookEvent): HookOutput;
|
|
50
51
|
export declare function runHookEvent(options: RunHookEventOptions): Promise<HookOutput>;
|
|
51
52
|
declare function recordProducedArtifacts(projectDir: string, ir: ConstraintIR, wfState: {
|
|
52
53
|
workflow: string;
|
package/dist/hooks/cli.js
CHANGED
|
@@ -18,17 +18,138 @@ import { mkdir, readFile, realpath, stat, unlink, writeFile } from "node:fs/prom
|
|
|
18
18
|
import { realpathSync } from "node:fs";
|
|
19
19
|
import { spawn } from "node:child_process";
|
|
20
20
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
21
|
-
import { randomUUID } from "node:crypto";
|
|
21
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { readStdin, writeOutput, silentOutput, allowOutput, blockOutput, stopOutput } from "./protocol.js";
|
|
24
24
|
import { validateHookInput } from "./schema.js";
|
|
25
25
|
import { enforcePreToolUse, enforcePostToolUse, enforceUserPromptSubmit, enforceSubagentStop, enforcePreCompact, enforceNotification, enforceSessionStart, enforceStop, extractBashWritePaths, checkReflectionLimit, checkContextReadiness, checkWorkflowBoundary, } from "./enforce.js";
|
|
26
|
-
import { appendAudit, readWorkflowState, appendTrace, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, writeArtifactManifest } from "./state.js";
|
|
26
|
+
import { appendAudit, readWorkflowState, appendTrace, appendRuntimeDecisionEvent, rotateTraces, readTraces, cleanStaleState, readSurgeonAttempts, writeSurgeonAttempts, appendSessionRead, readSessionReads, appendVerifierResult, appendCompletedArtifact, artifactIdentity, buildArtifactKey, readArtifactManifest, resolveArtifactTemplate, safePathComponent, writeArtifactManifest } from "./state.js";
|
|
27
27
|
import { hookEventsForSurface } from "./event-registry.js";
|
|
28
28
|
import { writeAuditEvent } from "../audit/index.js";
|
|
29
|
+
import { RUNTIME_DECISION_EVENT_SCHEMA_VERSION } from "../governance/index.js";
|
|
29
30
|
// ── Constants ──────────────────────────────────────────────
|
|
30
31
|
const DEFAULT_IR_PATH = ".dna/compiled/ir.json";
|
|
31
32
|
const VALID_EVENTS = new Set(hookEventsForSurface("cli"));
|
|
33
|
+
// ── Runtime Decision Evidence ────────────────────────────
|
|
34
|
+
function normalizeDecision(output) {
|
|
35
|
+
if (output.continue === false)
|
|
36
|
+
return "block";
|
|
37
|
+
if (output.hookSpecificOutput?.permissionDecision === "ask")
|
|
38
|
+
return "escalate";
|
|
39
|
+
if (output.hookSpecificOutput?.additionalContext?.startsWith("WARN"))
|
|
40
|
+
return "warn";
|
|
41
|
+
return "allow";
|
|
42
|
+
}
|
|
43
|
+
function hashedRef(value) {
|
|
44
|
+
const raw = typeof value === "string" && value.length > 0 ? value : "unknown";
|
|
45
|
+
return `sha256:${createHash("sha256").update(raw, "utf-8").digest("hex")}`;
|
|
46
|
+
}
|
|
47
|
+
function resourceRef(rawInput) {
|
|
48
|
+
const toolInput = typeof rawInput.tool_input === "object" && rawInput.tool_input !== null
|
|
49
|
+
? rawInput.tool_input
|
|
50
|
+
: {};
|
|
51
|
+
return hashedRef(toolInput.file_path ?? toolInput.notebook_path ?? toolInput.command ?? rawInput.message ?? rawInput.prompt ?? "unknown");
|
|
52
|
+
}
|
|
53
|
+
function actionKind(rawInput) {
|
|
54
|
+
const toolName = typeof rawInput.tool_name === "string" ? rawInput.tool_name : "unknown";
|
|
55
|
+
if (["Edit", "Write", "NotebookEdit"].includes(toolName))
|
|
56
|
+
return "file_write";
|
|
57
|
+
if (toolName === "Bash")
|
|
58
|
+
return "shell_command";
|
|
59
|
+
return toolName === "unknown" ? "unknown" : "tool_use";
|
|
60
|
+
}
|
|
61
|
+
function cascadeLayers(ir) {
|
|
62
|
+
const layers = ir.provenance?.cascade_layers;
|
|
63
|
+
const result = {};
|
|
64
|
+
for (const layer of ["species", "enterprise", "project", "personal", "role", "context", "task"]) {
|
|
65
|
+
const entry = layers?.[layer];
|
|
66
|
+
result[layer] = {
|
|
67
|
+
layer,
|
|
68
|
+
source_id: entry?.source_id ?? "unknown",
|
|
69
|
+
source_ref: entry?.source_ref ?? "unknown",
|
|
70
|
+
fingerprint: entry?.fingerprint ?? "unknown",
|
|
71
|
+
status: entry?.status ?? "unknown",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function fallbackConstraint(decision, reason, ir) {
|
|
77
|
+
return {
|
|
78
|
+
constraint_id: decision === "allow" ? "allow:no_blocking_constraint" : "unknown",
|
|
79
|
+
source_layer: decision === "allow" && ir?.provenance?.policy_bundle_id ? "enterprise" : "unknown",
|
|
80
|
+
source_dna_id: decision === "allow" ? ir?.provenance?.policy_bundle_id ?? "unknown" : "unknown",
|
|
81
|
+
gene_id: decision === "allow" ? "no_blocking_constraint" : "unknown",
|
|
82
|
+
codon_type: "threshold",
|
|
83
|
+
action: decision,
|
|
84
|
+
provenance_reason: reason || "unknown",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function candidateConstraintMatches(candidate, decision, result) {
|
|
88
|
+
if (candidate.action !== decision)
|
|
89
|
+
return false;
|
|
90
|
+
const geneId = result?.trace?.gene_id;
|
|
91
|
+
return !geneId || candidate.gene_id === geneId;
|
|
92
|
+
}
|
|
93
|
+
function winningConstraint(ir, decision, reason, result) {
|
|
94
|
+
const candidates = ir.provenance?.constraint_winners ?? [];
|
|
95
|
+
if (decision === "allow")
|
|
96
|
+
return fallbackConstraint(decision, reason, ir);
|
|
97
|
+
const matched = candidates.find((candidate) => candidateConstraintMatches(candidate, decision, result))
|
|
98
|
+
?? candidates.find((candidate) => candidate.action === decision);
|
|
99
|
+
if (!matched)
|
|
100
|
+
return fallbackConstraint(decision, reason, ir);
|
|
101
|
+
return { ...matched, action: decision };
|
|
102
|
+
}
|
|
103
|
+
function identityField(rawInput, inputKey, envKey) {
|
|
104
|
+
const value = rawInput[inputKey];
|
|
105
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
106
|
+
return value;
|
|
107
|
+
return process.env[envKey] ?? "unknown";
|
|
108
|
+
}
|
|
109
|
+
function buildRuntimeDecisionEvent(params) {
|
|
110
|
+
const agentType = typeof params.rawInput.agent_type === "string" ? params.rawInput.agent_type : "unknown";
|
|
111
|
+
const derivedRole = agentType.startsWith("dna-") ? agentType.slice("dna-".length) : agentType;
|
|
112
|
+
const agentRole = params.wfState?.current_role ?? params.result?.trace?.role ?? (derivedRole === "unknown" ? "unknown" : derivedRole);
|
|
113
|
+
const sessionId = params.sessionId ?? "unknown";
|
|
114
|
+
return {
|
|
115
|
+
schema_version: RUNTIME_DECISION_EVENT_SCHEMA_VERSION,
|
|
116
|
+
event_id: randomUUID(),
|
|
117
|
+
timestamp: new Date().toISOString(),
|
|
118
|
+
org_id: identityField(params.rawInput, "org_id", "INTENTDNA_ORG_ID"),
|
|
119
|
+
team_id: identityField(params.rawInput, "team_id", "INTENTDNA_TEAM_ID"),
|
|
120
|
+
user_id: identityField(params.rawInput, "user_id", "INTENTDNA_USER_ID"),
|
|
121
|
+
project_id: params.projectDir,
|
|
122
|
+
agent_id: agentType,
|
|
123
|
+
agent_role: agentRole,
|
|
124
|
+
agent_type: agentType,
|
|
125
|
+
session_agent: sessionId,
|
|
126
|
+
harness_adapter: "claude-code",
|
|
127
|
+
runtime: "hook",
|
|
128
|
+
policy_bundle_id: params.ir.provenance?.policy_bundle_id ?? "unknown",
|
|
129
|
+
policy_bundle_version: params.ir.provenance?.policy_bundle_version ?? "unknown",
|
|
130
|
+
cascade_layers: cascadeLayers(params.ir),
|
|
131
|
+
winning_constraint: winningConstraint(params.ir, params.decision, params.reason, params.result),
|
|
132
|
+
compiled_ir_hash: params.ir.compiled_ir_hash ?? params.ir.provenance?.compiled_ir_hash ?? "unknown",
|
|
133
|
+
decision: params.decision,
|
|
134
|
+
decision_reason: params.reason || "unknown",
|
|
135
|
+
enforcement_point: "hook",
|
|
136
|
+
harness_runtime_context: {
|
|
137
|
+
agent_id: agentType,
|
|
138
|
+
agent_role: agentRole,
|
|
139
|
+
agent_type: agentType,
|
|
140
|
+
session_agent: sessionId,
|
|
141
|
+
harness_adapter: "claude-code",
|
|
142
|
+
runtime: "hook",
|
|
143
|
+
},
|
|
144
|
+
session_id: sessionId,
|
|
145
|
+
run_id: params.wfState?.workflow ?? sessionId,
|
|
146
|
+
step_id: params.wfState?.current_step ?? params.result?.trace?.step_id ?? `${params.event}:direct`,
|
|
147
|
+
tool_name: typeof params.rawInput.tool_name === "string" ? params.rawInput.tool_name : "unknown",
|
|
148
|
+
action_kind: actionKind(params.rawInput),
|
|
149
|
+
resource_ref: resourceRef(params.rawInput),
|
|
150
|
+
evidence_refs: [{ type: "trace", ref: params.traceId }],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
32
153
|
export function computeSummary(traces) {
|
|
33
154
|
let blocks = 0, warns = 0;
|
|
34
155
|
const toolCounts = new Map();
|
|
@@ -106,15 +227,45 @@ async function main() {
|
|
|
106
227
|
const output = await runHookEvent({ event, ir, rawInput, projectDir, sessionId });
|
|
107
228
|
writeOutput(output);
|
|
108
229
|
}
|
|
230
|
+
export function normalizeHookOutputForEvent(output, event) {
|
|
231
|
+
if (!output.hookSpecificOutput)
|
|
232
|
+
return output;
|
|
233
|
+
return {
|
|
234
|
+
...output,
|
|
235
|
+
hookSpecificOutput: {
|
|
236
|
+
...output.hookSpecificOutput,
|
|
237
|
+
hookEventName: output.hookSpecificOutput.hookEventName ?? event,
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
109
241
|
export async function runHookEvent(options) {
|
|
110
242
|
const { event, ir, rawInput, projectDir, sessionId, roles } = options;
|
|
243
|
+
const finish = (output) => normalizeHookOutputForEvent(output, event);
|
|
111
244
|
const state = {};
|
|
245
|
+
const writeRuntimeDecision = async (output, result, wfState, traceId = randomUUID()) => {
|
|
246
|
+
const decision = normalizeDecision(output);
|
|
247
|
+
const reason = decision !== "allow" ? (output.reason ?? output.hookSpecificOutput?.additionalContext ?? "unknown") : "allowed by active policy";
|
|
248
|
+
await appendRuntimeDecisionEvent(projectDir, buildRuntimeDecisionEvent({
|
|
249
|
+
ir,
|
|
250
|
+
rawInput,
|
|
251
|
+
output,
|
|
252
|
+
result,
|
|
253
|
+
event,
|
|
254
|
+
projectDir,
|
|
255
|
+
sessionId,
|
|
256
|
+
traceId,
|
|
257
|
+
decision,
|
|
258
|
+
reason,
|
|
259
|
+
wfState,
|
|
260
|
+
}), sessionId);
|
|
261
|
+
};
|
|
112
262
|
let wfStateRaw = null;
|
|
113
263
|
// Load workflow state for events that need it (handoff context + PreCompact preservation)
|
|
114
264
|
if (event === "PreToolUse" || event === "PostToolUse" || event === "PreCompact") {
|
|
115
265
|
const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
|
|
116
266
|
if ("output" in workflowState) {
|
|
117
|
-
|
|
267
|
+
await writeRuntimeDecision(workflowState.output, { output: workflowState.output, trace: { matched_rule: "workflow_boundary" } }, null);
|
|
268
|
+
return finish(workflowState.output);
|
|
118
269
|
}
|
|
119
270
|
wfStateRaw = workflowState.state;
|
|
120
271
|
if (wfStateRaw && wfStateRaw.active) {
|
|
@@ -128,7 +279,8 @@ export async function runHookEvent(options) {
|
|
|
128
279
|
const artifactFacts = await resolveWorkflowArtifactFactsForHook(projectDir, ir, wfStateRaw, event, sessionId);
|
|
129
280
|
if ("output" in artifactFacts) {
|
|
130
281
|
appendArtifactResolverTrace(projectDir, event, wfStateRaw, artifactFacts.output, sessionId);
|
|
131
|
-
|
|
282
|
+
await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfStateRaw.current_step } }, wfStateRaw);
|
|
283
|
+
return finish(artifactFacts.output);
|
|
132
284
|
}
|
|
133
285
|
state.artifactFacts = artifactFacts.facts;
|
|
134
286
|
}
|
|
@@ -144,8 +296,9 @@ export async function runHookEvent(options) {
|
|
|
144
296
|
if (event === "PreToolUse") {
|
|
145
297
|
const gateResult = await handlePreToolGates(ir, rawInput, wfStateRaw, projectDir, sessionId);
|
|
146
298
|
if (gateResult) {
|
|
299
|
+
const traceId = randomUUID();
|
|
147
300
|
appendTrace(projectDir, {
|
|
148
|
-
trace_id:
|
|
301
|
+
trace_id: traceId,
|
|
149
302
|
event,
|
|
150
303
|
tool_name: typeof rawInput.tool_name === "string" ? rawInput.tool_name : undefined,
|
|
151
304
|
agent_type: typeof rawInput.agent_type === "string" ? rawInput.agent_type : undefined,
|
|
@@ -156,14 +309,16 @@ export async function runHookEvent(options) {
|
|
|
156
309
|
duration_ms: 0,
|
|
157
310
|
timestamp: new Date().toISOString(),
|
|
158
311
|
}, sessionId).catch(() => { });
|
|
159
|
-
|
|
312
|
+
await writeRuntimeDecision(gateResult.output, { output: gateResult.output, trace: { matched_rule: gateResult.matched_rule, step_id: wfStateRaw?.current_step } }, wfStateRaw, traceId);
|
|
313
|
+
return finish(gateResult.output);
|
|
160
314
|
}
|
|
161
315
|
}
|
|
162
316
|
// Special handling for Stop — needs async workflow state read + session summary
|
|
163
317
|
if (event === "Stop") {
|
|
164
318
|
const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
|
|
165
319
|
if ("output" in workflowState) {
|
|
166
|
-
|
|
320
|
+
await writeRuntimeDecision(workflowState.output, { output: workflowState.output, trace: { matched_rule: "workflow_boundary" } }, null);
|
|
321
|
+
return finish(workflowState.output);
|
|
167
322
|
}
|
|
168
323
|
const wfState = workflowState.state;
|
|
169
324
|
let stopArtifactFacts = [];
|
|
@@ -171,7 +326,8 @@ export async function runHookEvent(options) {
|
|
|
171
326
|
const artifactFacts = await finalizeAndResolveArtifactsForHook(projectDir, ir, wfState, event, sessionId);
|
|
172
327
|
if ("output" in artifactFacts) {
|
|
173
328
|
appendArtifactResolverTrace(projectDir, event, wfState, artifactFacts.output, sessionId);
|
|
174
|
-
|
|
329
|
+
await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfState.current_step } }, wfState);
|
|
330
|
+
return finish(artifactFacts.output);
|
|
175
331
|
}
|
|
176
332
|
stopArtifactFacts = artifactFacts.facts;
|
|
177
333
|
}
|
|
@@ -220,8 +376,9 @@ export async function runHookEvent(options) {
|
|
|
220
376
|
}
|
|
221
377
|
catch { /* fail-open */ }
|
|
222
378
|
// Trace for Stop
|
|
379
|
+
const traceId = randomUUID();
|
|
223
380
|
appendTrace(projectDir, {
|
|
224
|
-
trace_id:
|
|
381
|
+
trace_id: traceId,
|
|
225
382
|
event: "Stop",
|
|
226
383
|
workflow: wfState?.workflow,
|
|
227
384
|
step: wfState?.current_step,
|
|
@@ -229,7 +386,8 @@ export async function runHookEvent(options) {
|
|
|
229
386
|
duration_ms: 0,
|
|
230
387
|
timestamp: new Date().toISOString(),
|
|
231
388
|
}, sessionId).catch(() => { });
|
|
232
|
-
|
|
389
|
+
await writeRuntimeDecision(stopOutput, { output: stopOutput, trace: { matched_rule: "validator", step_id: wfState?.current_step } }, wfState, traceId);
|
|
390
|
+
return finish(stopOutput);
|
|
233
391
|
}
|
|
234
392
|
// Dispatch to enforcement engine with timing
|
|
235
393
|
const start = Date.now();
|
|
@@ -306,24 +464,27 @@ export async function runHookEvent(options) {
|
|
|
306
464
|
catch { /* fail-open: pattern detection never blocks */ }
|
|
307
465
|
}
|
|
308
466
|
// Trace logging (async, fail-open)
|
|
309
|
-
const decision = output
|
|
310
|
-
|
|
311
|
-
|
|
467
|
+
const decision = normalizeDecision(output);
|
|
468
|
+
const traceDecision = decision === "block" ? "block" : decision === "allow" ? "allow" : "warn";
|
|
469
|
+
const traceId = randomUUID();
|
|
470
|
+
const decisionReason = decision !== "allow"
|
|
471
|
+
? (output.reason ?? output.hookSpecificOutput?.additionalContext ?? "unknown")
|
|
472
|
+
: "allowed by active policy";
|
|
473
|
+
const traceTimestamp = new Date().toISOString();
|
|
312
474
|
appendTrace(projectDir, {
|
|
313
|
-
trace_id:
|
|
475
|
+
trace_id: traceId,
|
|
314
476
|
event,
|
|
315
477
|
tool_name: typeof rawInput.tool_name === "string" ? rawInput.tool_name : undefined,
|
|
316
478
|
agent_type: typeof rawInput.agent_type === "string" ? rawInput.agent_type : undefined,
|
|
317
479
|
workflow: state.workflowState?.workflow,
|
|
318
480
|
step: state.workflowState?.current_step,
|
|
319
|
-
decision:
|
|
320
|
-
reason: decision !== "allow"
|
|
321
|
-
? (output.reason ?? output.hookSpecificOutput?.additionalContext)
|
|
322
|
-
: undefined,
|
|
481
|
+
decision: traceDecision,
|
|
482
|
+
reason: decision !== "allow" ? decisionReason : undefined,
|
|
323
483
|
target_path: decision !== "allow" ? targetPath : undefined,
|
|
324
484
|
duration_ms: durationMs,
|
|
325
|
-
timestamp:
|
|
485
|
+
timestamp: traceTimestamp,
|
|
326
486
|
}, sessionId).catch(() => { }); // Fail-open
|
|
487
|
+
await writeRuntimeDecision(output, result, state.workflowState, traceId);
|
|
327
488
|
// Side effect: rotate traces + clean stale state on SessionStart
|
|
328
489
|
if (event === "SessionStart") {
|
|
329
490
|
rotateTraces(projectDir).catch(() => { });
|
|
@@ -339,7 +500,7 @@ export async function runHookEvent(options) {
|
|
|
339
500
|
session_id: sessionId,
|
|
340
501
|
}).catch(() => { }); // Fail-open
|
|
341
502
|
}
|
|
342
|
-
return output;
|
|
503
|
+
return finish(output);
|
|
343
504
|
}
|
|
344
505
|
// ── Dispatch ───────────────────────────────────────────────
|
|
345
506
|
function dispatch(event, ir, input, state, roles) {
|
package/dist/hooks/state.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* All write operations use atomic temp+rename pattern.
|
|
11
11
|
*/
|
|
12
12
|
import type { ArtifactFact, ArtifactKey, ArtifactManifest, CompletedArtifactEntry, HandoffArtifact, HandoffType, VerifierKind, VerifierSeverity, VerifierWhen } from "../schema/types.js";
|
|
13
|
+
import { type RuntimeDecisionEvent } from "../governance/index.js";
|
|
13
14
|
/** Workflow state written by runtime and read by hooks */
|
|
14
15
|
export interface DNAWorkflowState {
|
|
15
16
|
active: boolean;
|
|
@@ -180,16 +181,8 @@ export interface TraceEntry {
|
|
|
180
181
|
* Fail-open: never throws.
|
|
181
182
|
*/
|
|
182
183
|
export declare function appendTrace(projectDir: string, entry: TraceEntry, sessionId?: string): Promise<void>;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
*
|
|
186
|
-
* G1 session isolation:
|
|
187
|
-
* - With sessionId: reads from `.dna/state/sessions/{id}/trace.jsonl`
|
|
188
|
-
* (falls back to legacy global files with sessionId in name for backward compat)
|
|
189
|
-
* - Without sessionId: reads from `.dna/state/trace/trace-{date}.jsonl` (global merged view)
|
|
190
|
-
*
|
|
191
|
-
* Returns parsed entries sorted by timestamp.
|
|
192
|
-
*/
|
|
184
|
+
export declare function appendRuntimeDecisionEvent(projectDir: string, event: RuntimeDecisionEvent, sessionId?: string): Promise<void>;
|
|
185
|
+
export declare function readRuntimeDecisionEvents(projectDir: string, days?: number, sessionId?: string): Promise<RuntimeDecisionEvent[]>;
|
|
193
186
|
export declare function readTraces(projectDir: string, days?: number, sessionId?: string): Promise<TraceEntry[]>;
|
|
194
187
|
/**
|
|
195
188
|
* List active session directories under `.dna/state/sessions/`.
|