intentdna 1.7.0 → 1.7.4
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 +3 -1
- 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.js +162 -13
- 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.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();
|
|
@@ -109,11 +230,29 @@ async function main() {
|
|
|
109
230
|
export async function runHookEvent(options) {
|
|
110
231
|
const { event, ir, rawInput, projectDir, sessionId, roles } = options;
|
|
111
232
|
const state = {};
|
|
233
|
+
const writeRuntimeDecision = async (output, result, wfState, traceId = randomUUID()) => {
|
|
234
|
+
const decision = normalizeDecision(output);
|
|
235
|
+
const reason = decision !== "allow" ? (output.reason ?? output.hookSpecificOutput?.additionalContext ?? "unknown") : "allowed by active policy";
|
|
236
|
+
await appendRuntimeDecisionEvent(projectDir, buildRuntimeDecisionEvent({
|
|
237
|
+
ir,
|
|
238
|
+
rawInput,
|
|
239
|
+
output,
|
|
240
|
+
result,
|
|
241
|
+
event,
|
|
242
|
+
projectDir,
|
|
243
|
+
sessionId,
|
|
244
|
+
traceId,
|
|
245
|
+
decision,
|
|
246
|
+
reason,
|
|
247
|
+
wfState,
|
|
248
|
+
}), sessionId);
|
|
249
|
+
};
|
|
112
250
|
let wfStateRaw = null;
|
|
113
251
|
// Load workflow state for events that need it (handoff context + PreCompact preservation)
|
|
114
252
|
if (event === "PreToolUse" || event === "PostToolUse" || event === "PreCompact") {
|
|
115
253
|
const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
|
|
116
254
|
if ("output" in workflowState) {
|
|
255
|
+
await writeRuntimeDecision(workflowState.output, { output: workflowState.output, trace: { matched_rule: "workflow_boundary" } }, null);
|
|
117
256
|
return workflowState.output;
|
|
118
257
|
}
|
|
119
258
|
wfStateRaw = workflowState.state;
|
|
@@ -128,6 +267,7 @@ export async function runHookEvent(options) {
|
|
|
128
267
|
const artifactFacts = await resolveWorkflowArtifactFactsForHook(projectDir, ir, wfStateRaw, event, sessionId);
|
|
129
268
|
if ("output" in artifactFacts) {
|
|
130
269
|
appendArtifactResolverTrace(projectDir, event, wfStateRaw, artifactFacts.output, sessionId);
|
|
270
|
+
await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfStateRaw.current_step } }, wfStateRaw);
|
|
131
271
|
return artifactFacts.output;
|
|
132
272
|
}
|
|
133
273
|
state.artifactFacts = artifactFacts.facts;
|
|
@@ -144,8 +284,9 @@ export async function runHookEvent(options) {
|
|
|
144
284
|
if (event === "PreToolUse") {
|
|
145
285
|
const gateResult = await handlePreToolGates(ir, rawInput, wfStateRaw, projectDir, sessionId);
|
|
146
286
|
if (gateResult) {
|
|
287
|
+
const traceId = randomUUID();
|
|
147
288
|
appendTrace(projectDir, {
|
|
148
|
-
trace_id:
|
|
289
|
+
trace_id: traceId,
|
|
149
290
|
event,
|
|
150
291
|
tool_name: typeof rawInput.tool_name === "string" ? rawInput.tool_name : undefined,
|
|
151
292
|
agent_type: typeof rawInput.agent_type === "string" ? rawInput.agent_type : undefined,
|
|
@@ -156,6 +297,7 @@ export async function runHookEvent(options) {
|
|
|
156
297
|
duration_ms: 0,
|
|
157
298
|
timestamp: new Date().toISOString(),
|
|
158
299
|
}, sessionId).catch(() => { });
|
|
300
|
+
await writeRuntimeDecision(gateResult.output, { output: gateResult.output, trace: { matched_rule: gateResult.matched_rule, step_id: wfStateRaw?.current_step } }, wfStateRaw, traceId);
|
|
159
301
|
return gateResult.output;
|
|
160
302
|
}
|
|
161
303
|
}
|
|
@@ -163,6 +305,7 @@ export async function runHookEvent(options) {
|
|
|
163
305
|
if (event === "Stop") {
|
|
164
306
|
const workflowState = await readWorkflowStateForHook(projectDir, event, sessionId);
|
|
165
307
|
if ("output" in workflowState) {
|
|
308
|
+
await writeRuntimeDecision(workflowState.output, { output: workflowState.output, trace: { matched_rule: "workflow_boundary" } }, null);
|
|
166
309
|
return workflowState.output;
|
|
167
310
|
}
|
|
168
311
|
const wfState = workflowState.state;
|
|
@@ -171,6 +314,7 @@ export async function runHookEvent(options) {
|
|
|
171
314
|
const artifactFacts = await finalizeAndResolveArtifactsForHook(projectDir, ir, wfState, event, sessionId);
|
|
172
315
|
if ("output" in artifactFacts) {
|
|
173
316
|
appendArtifactResolverTrace(projectDir, event, wfState, artifactFacts.output, sessionId);
|
|
317
|
+
await writeRuntimeDecision(artifactFacts.output, { output: artifactFacts.output, trace: { matched_rule: "handoff", step_id: wfState.current_step } }, wfState);
|
|
174
318
|
return artifactFacts.output;
|
|
175
319
|
}
|
|
176
320
|
stopArtifactFacts = artifactFacts.facts;
|
|
@@ -220,8 +364,9 @@ export async function runHookEvent(options) {
|
|
|
220
364
|
}
|
|
221
365
|
catch { /* fail-open */ }
|
|
222
366
|
// Trace for Stop
|
|
367
|
+
const traceId = randomUUID();
|
|
223
368
|
appendTrace(projectDir, {
|
|
224
|
-
trace_id:
|
|
369
|
+
trace_id: traceId,
|
|
225
370
|
event: "Stop",
|
|
226
371
|
workflow: wfState?.workflow,
|
|
227
372
|
step: wfState?.current_step,
|
|
@@ -229,6 +374,7 @@ export async function runHookEvent(options) {
|
|
|
229
374
|
duration_ms: 0,
|
|
230
375
|
timestamp: new Date().toISOString(),
|
|
231
376
|
}, sessionId).catch(() => { });
|
|
377
|
+
await writeRuntimeDecision(stopOutput, { output: stopOutput, trace: { matched_rule: "validator", step_id: wfState?.current_step } }, wfState, traceId);
|
|
232
378
|
return stopOutput;
|
|
233
379
|
}
|
|
234
380
|
// Dispatch to enforcement engine with timing
|
|
@@ -306,24 +452,27 @@ export async function runHookEvent(options) {
|
|
|
306
452
|
catch { /* fail-open: pattern detection never blocks */ }
|
|
307
453
|
}
|
|
308
454
|
// Trace logging (async, fail-open)
|
|
309
|
-
const decision = output
|
|
310
|
-
|
|
311
|
-
|
|
455
|
+
const decision = normalizeDecision(output);
|
|
456
|
+
const traceDecision = decision === "block" ? "block" : decision === "allow" ? "allow" : "warn";
|
|
457
|
+
const traceId = randomUUID();
|
|
458
|
+
const decisionReason = decision !== "allow"
|
|
459
|
+
? (output.reason ?? output.hookSpecificOutput?.additionalContext ?? "unknown")
|
|
460
|
+
: "allowed by active policy";
|
|
461
|
+
const traceTimestamp = new Date().toISOString();
|
|
312
462
|
appendTrace(projectDir, {
|
|
313
|
-
trace_id:
|
|
463
|
+
trace_id: traceId,
|
|
314
464
|
event,
|
|
315
465
|
tool_name: typeof rawInput.tool_name === "string" ? rawInput.tool_name : undefined,
|
|
316
466
|
agent_type: typeof rawInput.agent_type === "string" ? rawInput.agent_type : undefined,
|
|
317
467
|
workflow: state.workflowState?.workflow,
|
|
318
468
|
step: state.workflowState?.current_step,
|
|
319
|
-
decision:
|
|
320
|
-
reason: decision !== "allow"
|
|
321
|
-
? (output.reason ?? output.hookSpecificOutput?.additionalContext)
|
|
322
|
-
: undefined,
|
|
469
|
+
decision: traceDecision,
|
|
470
|
+
reason: decision !== "allow" ? decisionReason : undefined,
|
|
323
471
|
target_path: decision !== "allow" ? targetPath : undefined,
|
|
324
472
|
duration_ms: durationMs,
|
|
325
|
-
timestamp:
|
|
473
|
+
timestamp: traceTimestamp,
|
|
326
474
|
}, sessionId).catch(() => { }); // Fail-open
|
|
475
|
+
await writeRuntimeDecision(output, result, state.workflowState, traceId);
|
|
327
476
|
// Side effect: rotate traces + clean stale state on SessionStart
|
|
328
477
|
if (event === "SessionStart") {
|
|
329
478
|
rotateTraces(projectDir).catch(() => { });
|
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/`.
|