@deftai/directive-core 0.88.0 → 0.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authz/actions.d.ts +55 -0
- package/dist/authz/actions.js +125 -0
- package/dist/authz/classify.d.ts +23 -0
- package/dist/authz/classify.js +217 -0
- package/dist/authz/closed-verb.d.ts +42 -0
- package/dist/authz/closed-verb.js +279 -0
- package/dist/authz/evaluate.d.ts +41 -0
- package/dist/authz/evaluate.js +298 -0
- package/dist/authz/index.d.ts +15 -0
- package/dist/authz/index.js +15 -0
- package/dist/authz/origin.d.ts +28 -0
- package/dist/authz/origin.js +64 -0
- package/dist/authz/paths.d.ts +6 -0
- package/dist/authz/paths.js +19 -0
- package/dist/authz/store.d.ts +40 -0
- package/dist/authz/store.js +317 -0
- package/dist/authz/templates.d.ts +139 -0
- package/dist/authz/templates.js +241 -0
- package/dist/authz/types.d.ts +111 -0
- package/dist/authz/types.js +41 -0
- package/dist/authz/verb-classification.d.ts +44 -0
- package/dist/authz/verb-classification.js +237 -0
- package/dist/branch/evaluate.js +6 -1
- package/dist/cache/fetch.js +10 -5
- package/dist/cache/io.d.ts +9 -2
- package/dist/cache/io.js +30 -10
- package/dist/cache/task-cache/store.js +11 -7
- package/dist/capacity/backfill.js +10 -4
- package/dist/category-b-namespace/index.js +10 -4
- package/dist/codebase/default-extractor.js +3 -0
- package/dist/codebase/map.js +11 -4
- package/dist/doctor/constants.d.ts +1 -1
- package/dist/doctor/constants.js +2 -0
- package/dist/doctor/doctor-state.js +28 -4
- package/dist/doctor/flags.js +21 -1
- package/dist/doctor/index.d.ts +1 -0
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/main.d.ts +10 -0
- package/dist/doctor/main.js +182 -0
- package/dist/doctor/openclaw-skills.d.ts +109 -0
- package/dist/doctor/openclaw-skills.js +463 -0
- package/dist/doctor/types.d.ts +22 -0
- package/dist/escalation/actions.d.ts +63 -0
- package/dist/escalation/actions.js +137 -0
- package/dist/escalation/index.d.ts +8 -0
- package/dist/escalation/index.js +8 -0
- package/dist/escalation/paths.d.ts +3 -0
- package/dist/escalation/paths.js +10 -0
- package/dist/escalation/store.d.ts +17 -0
- package/dist/escalation/store.js +172 -0
- package/dist/escalation/types.d.ts +73 -0
- package/dist/escalation/types.js +43 -0
- package/dist/eval/crud-telemetry.js +30 -10
- package/dist/eval/health.js +22 -7
- package/dist/eval/readback.js +11 -10
- package/dist/eval/run.js +32 -16
- package/dist/events/attribution-enrichment.js +10 -4
- package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
- package/dist/finish-loop/directive-finish-loop.js +239 -0
- package/dist/finish-loop/grant-gate.d.ts +31 -0
- package/dist/finish-loop/grant-gate.js +169 -0
- package/dist/finish-loop/index.d.ts +11 -0
- package/dist/finish-loop/index.js +11 -0
- package/dist/finish-loop/main.d.ts +35 -0
- package/dist/finish-loop/main.js +365 -0
- package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
- package/dist/finish-loop/pr-finish-loop.js +217 -0
- package/dist/finish-loop/progress.d.ts +17 -0
- package/dist/finish-loop/progress.js +45 -0
- package/dist/finish-loop/queue.d.ts +16 -0
- package/dist/finish-loop/queue.js +65 -0
- package/dist/finish-loop/types.d.ts +52 -0
- package/dist/finish-loop/types.js +10 -0
- package/dist/fs/contained-write.d.ts +101 -0
- package/dist/fs/contained-write.js +281 -0
- package/dist/hooks/classify/classify.d.ts +10 -0
- package/dist/hooks/classify/classify.js +37 -0
- package/dist/hooks/classify/index.d.ts +14 -0
- package/dist/hooks/classify/index.js +13 -0
- package/dist/hooks/classify/paths.d.ts +22 -0
- package/dist/hooks/classify/paths.js +72 -0
- package/dist/hooks/classify/payload.d.ts +16 -0
- package/dist/hooks/classify/payload.js +45 -0
- package/dist/hooks/classify/stdin.d.ts +12 -0
- package/dist/hooks/classify/stdin.js +63 -0
- package/dist/hooks/classify/tool-name.d.ts +18 -0
- package/dist/hooks/classify/tool-name.js +85 -0
- package/dist/hooks/classify/types.d.ts +41 -0
- package/dist/hooks/classify/types.js +7 -0
- package/dist/hooks/dispatcher.d.ts +21 -18
- package/dist/hooks/dispatcher.js +232 -168
- package/dist/hooks/fixtures/cases.d.ts +44 -0
- package/dist/hooks/fixtures/cases.js +595 -0
- package/dist/hooks/fixtures/index.d.ts +2 -0
- package/dist/hooks/fixtures/index.js +2 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/init-deposit/agent-hooks.js +27 -8
- package/dist/init-deposit/gitignore.js +11 -3
- package/dist/init-deposit/headless-manifest.js +12 -4
- package/dist/init-deposit/migrate.d.ts +1 -1
- package/dist/init-deposit/migrate.js +13 -3
- package/dist/init-deposit/prettierignore.js +10 -3
- package/dist/init-deposit/scaffold.js +35 -26
- package/dist/init-deposit/xbrief-projections.js +11 -4
- package/dist/intake/candidates-log.js +46 -35
- package/dist/intake/github-body-cli.d.ts +6 -0
- package/dist/intake/github-body-cli.js +17 -0
- package/dist/intake/github-body.d.ts +46 -3
- package/dist/intake/github-body.js +191 -20
- package/dist/intake/issue-emit.d.ts +1 -0
- package/dist/intake/issue-emit.js +41 -23
- package/dist/lifecycle/event-detect.js +12 -4
- package/dist/lifecycle/events.js +31 -20
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/lifecycle/lifecycle-hygiene.js +9 -3
- package/dist/lifecycle/stats.d.ts +53 -0
- package/dist/lifecycle/stats.js +286 -0
- package/dist/packs/pack-render.js +19 -6
- package/dist/plan-sequence/store.js +11 -4
- package/dist/policy/hotfix-criteria.d.ts +79 -0
- package/dist/policy/hotfix-criteria.js +197 -0
- package/dist/policy/index.d.ts +4 -0
- package/dist/policy/index.js +31 -1
- package/dist/policy/intent-ceiling.d.ts +79 -0
- package/dist/policy/intent-ceiling.js +181 -0
- package/dist/policy/no-deft-directive.js +10 -3
- package/dist/policy/org-force-on-migration.js +9 -5
- package/dist/policy/require-human-merge.d.ts +75 -0
- package/dist/policy/require-human-merge.js +324 -0
- package/dist/policy/resolve.js +17 -6
- package/dist/policy/runtime-authority.d.ts +15 -2
- package/dist/policy/runtime-authority.js +23 -3
- package/dist/policy/write-fence.d.ts +78 -0
- package/dist/policy/write-fence.js +164 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
- package/dist/pr-wait-mergeable/cascade.js +28 -0
- package/dist/preflight/evaluate.js +10 -0
- package/dist/product-signal/consent.js +21 -5
- package/dist/product-signal/submit.js +22 -12
- package/dist/release/build-dist-runner.js +5 -2
- package/dist/release/build-dist.d.ts +19 -1
- package/dist/release/build-dist.js +50 -2
- package/dist/release/native-steps.js +7 -2
- package/dist/release/spawn.js +13 -0
- package/dist/release-e2e/git-ops.d.ts +7 -0
- package/dist/release-e2e/git-ops.js +35 -2
- package/dist/release-publish/pipeline.d.ts +13 -0
- package/dist/release-publish/pipeline.js +46 -1
- package/dist/scope/audit-log.js +19 -24
- package/dist/scope/decompose.js +10 -5
- package/dist/scope/decomposed-refs.js +18 -3
- package/dist/scope/demote.js +9 -2
- package/dist/scope/undo.js +9 -2
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/process-cost-constants.d.ts +9 -0
- package/dist/session/process-cost-constants.js +11 -0
- package/dist/session/process-cost.d.ts +53 -0
- package/dist/session/process-cost.js +82 -0
- package/dist/session/release-availability.js +26 -6
- package/dist/session/ritual-sentinel.d.ts +5 -0
- package/dist/session/ritual-sentinel.js +31 -13
- package/dist/session/session-ready.d.ts +67 -0
- package/dist/session/session-ready.js +264 -0
- package/dist/session/session-start.d.ts +56 -1
- package/dist/session/session-start.js +384 -23
- package/dist/session/verify-session-ritual.d.ts +8 -0
- package/dist/session/verify-session-ritual.js +95 -35
- package/dist/staleness-tickler/state.js +26 -6
- package/dist/swarm/launch.js +13 -3
- package/dist/swarm/routing.js +9 -3
- package/dist/task-surface/index.js +24 -9
- package/dist/tool-events/classify.d.ts +20 -0
- package/dist/tool-events/classify.js +634 -0
- package/dist/tool-events/index.d.ts +10 -0
- package/dist/tool-events/index.js +10 -0
- package/dist/tool-events/summarize.d.ts +34 -0
- package/dist/tool-events/summarize.js +93 -0
- package/dist/tool-events/types.d.ts +52 -0
- package/dist/tool-events/types.js +13 -0
- package/dist/triage/bootstrap/gitignore.js +53 -10
- package/dist/triage/cache-path.js +10 -3
- package/dist/triage/welcome/summary.js +11 -4
- package/dist/triage/welcome/writers.js +45 -16
- package/dist/validate-content/validate-links.js +5 -0
- package/dist/value/readback.js +11 -6
- package/dist/vbrief-activate/activate.js +12 -4
- package/dist/vbrief-build/project-definition-io.js +14 -6
- package/dist/verify-source/contained-writes.d.ts +59 -0
- package/dist/verify-source/contained-writes.js +272 -0
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/openclaw-tier1.js +14 -1
- package/dist/verify-source/verify-stubs.js +3 -0
- package/dist/xbrief-migrate/migrate-project.js +26 -12
- package/dist/xbrief-migrate/transforms.d.ts +4 -2
- package/dist/xbrief-migrate/transforms.js +37 -14
- package/package.json +19 -3
package/dist/hooks/dispatcher.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { appendAuthzAudit, classifyHookAuthzOps, evaluateAuthzMutation, evidenceSatisfiesImplementationApproval, listActiveHumanGrants, loadAuthzStateResult, markGrantUsed, shouldConsumeSingleUseGrant, utcIso, } from "../authz/index.js";
|
|
3
4
|
import { hasArtifactSuffix } from "../layout/resolve.js";
|
|
5
|
+
import { evaluateIntentCeilingFromEnv } from "../policy/intent-ceiling.js";
|
|
4
6
|
import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, } from "../policy/no-deft-directive.js";
|
|
5
|
-
import { classifyMcpTool, evaluateRuntimeAuthorityDirectWrite, evaluateRuntimeAuthorityShellOp, listShellOps, loadRuntimeAuthorityFromProject, } from "../policy/runtime-authority.js";
|
|
7
|
+
import { classifyMcpTool, DEFAULT_RUNTIME_AUTHORITY_POLICY, evaluateRuntimeAuthorityDirectWrite, evaluateRuntimeAuthorityShellOp, listShellOps, loadRuntimeAuthorityFromProject, } from "../policy/runtime-authority.js";
|
|
8
|
+
import { loadStoryWriteFenceFromPath, resolveWriteFence } from "../policy/write-fence.js";
|
|
9
|
+
import { detectBranch } from "../session/git.js";
|
|
10
|
+
import { emitSessionRitualBlockedProcessCost } from "../session/process-cost.js";
|
|
6
11
|
import { markRitualStaleAfterCompact } from "../session/ritual-sentinel.js";
|
|
7
12
|
import { runSessionStartHookWrite } from "../session/session-start-hook.js";
|
|
8
|
-
import { inspectSessionRitual } from "../session/verify-session-ritual.js";
|
|
13
|
+
import { formatRitualRecoveryInstruction, inspectSessionRitual, } from "../session/verify-session-ritual.js";
|
|
14
|
+
import { hookMcpArgsText, hookShellCommand, hookToolName, hookWriteTargetPath, missingToolNameMessage, record, } from "./classify/index.js";
|
|
9
15
|
import { isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
|
|
10
16
|
import { inspectActiveScope } from "./scope.js";
|
|
11
17
|
import { isDirectWriteTool, isMcpTool, isShellTool, isSpawnTool } from "./tools.js";
|
|
18
|
+
// Pure parse/classify helpers are defined in ./classify/ and re-exported from
|
|
19
|
+
// ./index.ts (#2950). Dispatcher is orchestration: classify → policy → decision.
|
|
12
20
|
export { hookReadOnlyFromPayload, isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
|
|
13
21
|
export { DIRECT_WRITE_HOOK_MATCHER, DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool, isMcpTool, isShellTool, isSpawnTool, MCP_HOOK_MATCHER, MCP_PUSH_MERGE_BARE_NAMES, READ_ONLY_HOOK_ENV, SHELL_HOOK_MATCHER, SHELL_TOOL_NAMES, SPAWN_HOOK_MATCHER, SPAWN_TOOL_NAMES, } from "./tools.js";
|
|
14
22
|
export const HOOK_HOSTS = ["claude", "grok", "cursor", "codex"];
|
|
@@ -17,138 +25,6 @@ export const HOOK_EVENTS = ["session.start", "session.compact", "tool.before"];
|
|
|
17
25
|
export const COMPACT_HOOK_HOSTS = ["claude", "grok", "cursor"];
|
|
18
26
|
/** Hosts without a native compact hook surface — deposit skips cleanly (#2113). */
|
|
19
27
|
export const COMPACT_HOOK_SKIP_HOSTS = ["codex"];
|
|
20
|
-
function record(value) {
|
|
21
|
-
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
22
|
-
? value
|
|
23
|
-
: null;
|
|
24
|
-
}
|
|
25
|
-
/** First non-empty trimmed string from candidates (array form — clear arity for static tools). */
|
|
26
|
-
function firstString(values) {
|
|
27
|
-
for (const value of values) {
|
|
28
|
-
if (typeof value === "string" && value.trim().length > 0)
|
|
29
|
-
return value.trim();
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
function fieldPresent(input, key) {
|
|
34
|
-
return key in input;
|
|
35
|
-
}
|
|
36
|
-
function fieldString(input, key) {
|
|
37
|
-
const value = input[key];
|
|
38
|
-
if (typeof value === "string" && value.trim().length > 0)
|
|
39
|
-
return value.trim();
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
function toolInputRecord(payload) {
|
|
43
|
-
const toolCall = record(payload.tool_call) ?? record(payload.toolCall);
|
|
44
|
-
return (record(payload.tool_input) ??
|
|
45
|
-
record(payload.toolInput) ??
|
|
46
|
-
record(payload.input) ??
|
|
47
|
-
record(payload.arguments) ??
|
|
48
|
-
(toolCall !== null ? record(toolCall.arguments) : null));
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Cursor preToolUse payloads sometimes omit `tool_name` even when the hook matcher
|
|
52
|
-
* fired for a direct-write tool (#2628). Infer from nested tool input when possible.
|
|
53
|
-
*/
|
|
54
|
-
function inferCursorDirectWriteToolName(payload) {
|
|
55
|
-
const toolInput = toolInputRecord(payload);
|
|
56
|
-
if (toolInput !== null) {
|
|
57
|
-
if (fieldPresent(toolInput, "new_string") ||
|
|
58
|
-
fieldPresent(toolInput, "newString") ||
|
|
59
|
-
fieldPresent(toolInput, "old_string") ||
|
|
60
|
-
fieldPresent(toolInput, "oldString")) {
|
|
61
|
-
return "StrReplace";
|
|
62
|
-
}
|
|
63
|
-
if (fieldString(toolInput, "contents") !== null ||
|
|
64
|
-
fieldString(toolInput, "content") !== null ||
|
|
65
|
-
fieldString(toolInput, "text") !== null) {
|
|
66
|
-
return "Write";
|
|
67
|
-
}
|
|
68
|
-
if (fieldString(toolInput, "patch") !== null ||
|
|
69
|
-
fieldString(toolInput, "unified_diff") !== null ||
|
|
70
|
-
fieldString(toolInput, "diff") !== null) {
|
|
71
|
-
return "ApplyPatch";
|
|
72
|
-
}
|
|
73
|
-
if (Array.isArray(toolInput.edits))
|
|
74
|
-
return "MultiEdit";
|
|
75
|
-
if (Array.isArray(toolInput.cells) || toolInput.cell_id != null) {
|
|
76
|
-
return "NotebookEdit";
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
// Cursor maps Claude Edit → Write; a write target without contents is still a direct write.
|
|
80
|
-
if (hookWriteTargetPath(payload) !== null)
|
|
81
|
-
return "Write";
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
export function hookPayloadTopLevelKeys(payload) {
|
|
85
|
-
const input = record(payload);
|
|
86
|
-
if (input === null)
|
|
87
|
-
return [];
|
|
88
|
-
return Object.keys(input).sort();
|
|
89
|
-
}
|
|
90
|
-
export function hookToolName(payload, host) {
|
|
91
|
-
const input = record(payload);
|
|
92
|
-
if (input === null)
|
|
93
|
-
return null;
|
|
94
|
-
const toolObject = record(input.tool);
|
|
95
|
-
const toolCall = record(input.tool_call) ?? record(input.toolCall);
|
|
96
|
-
// OpenAI-style nestings are host-agnostic; checked before Cursor-only inference.
|
|
97
|
-
const direct = fieldString(input, "tool_name") ??
|
|
98
|
-
fieldString(input, "toolName") ??
|
|
99
|
-
fieldString(input, "tool") ??
|
|
100
|
-
(toolObject !== null ? fieldString(toolObject, "name") : null) ??
|
|
101
|
-
(toolCall !== null ? fieldString(toolCall, "name") : null);
|
|
102
|
-
if (direct !== null)
|
|
103
|
-
return direct;
|
|
104
|
-
if (host === "cursor")
|
|
105
|
-
return inferCursorDirectWriteToolName(input);
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
function missingToolNameMessage(input) {
|
|
109
|
-
const { host, payload, context } = input;
|
|
110
|
-
if (host === "cursor") {
|
|
111
|
-
if (context?.stdinEmpty) {
|
|
112
|
-
return ("Directive denied this Cursor preToolUse event because the host sent an empty payload " +
|
|
113
|
-
"(stdin was empty — not a session ritual or scope failure). " +
|
|
114
|
-
"If write tools should pass, update Directive or report the payload shape from Cursor.");
|
|
115
|
-
}
|
|
116
|
-
if (context?.parseFailed) {
|
|
117
|
-
return ("Directive denied this Cursor preToolUse event because the host payload was not valid JSON " +
|
|
118
|
-
"(host-integration mismatch — not a session ritual or scope failure). " +
|
|
119
|
-
"If write tools should pass, update Directive or report the payload shape from Cursor.");
|
|
120
|
-
}
|
|
121
|
-
const keys = hookPayloadTopLevelKeys(payload);
|
|
122
|
-
if (keys.length > 0) {
|
|
123
|
-
return ("Directive denied this Cursor preToolUse event because the host payload omitted a " +
|
|
124
|
-
"recognizable tool name (host-integration mismatch — not a session ritual or scope failure). " +
|
|
125
|
-
`Top-level payload keys: ${keys.join(", ")}. ` +
|
|
126
|
-
"If write tools should pass, update Directive or report the payload shape from Cursor.");
|
|
127
|
-
}
|
|
128
|
-
return ("Directive denied this Cursor preToolUse event because the host payload omitted a " +
|
|
129
|
-
"recognizable tool name (host-integration mismatch — not a session ritual or scope failure). " +
|
|
130
|
-
"If write tools should pass, update Directive or report the payload shape from Cursor.");
|
|
131
|
-
}
|
|
132
|
-
return "Directive denied this matched write event because the host payload omitted its tool name.";
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Best-effort write-target path from host PreToolUse payloads (#2625).
|
|
136
|
-
* Hosts disagree on nesting (`tool_input.file_path` vs top-level `path`).
|
|
137
|
-
*/
|
|
138
|
-
export function hookWriteTargetPath(payload) {
|
|
139
|
-
const input = record(payload);
|
|
140
|
-
if (input === null)
|
|
141
|
-
return null;
|
|
142
|
-
const toolInput = toolInputRecord(input);
|
|
143
|
-
return firstString([
|
|
144
|
-
toolInput?.file_path,
|
|
145
|
-
toolInput?.filePath,
|
|
146
|
-
toolInput?.path,
|
|
147
|
-
input.file_path,
|
|
148
|
-
input.filePath,
|
|
149
|
-
input.path,
|
|
150
|
-
]);
|
|
151
|
-
}
|
|
152
28
|
/** POSIX-ish project-relative path for lifecycle matching. */
|
|
153
29
|
export function toProjectRelativePosix(projectRoot, targetPath) {
|
|
154
30
|
const abs = resolve(projectRoot, targetPath.replace(/\\/g, "/"));
|
|
@@ -299,52 +175,171 @@ function deny(input, code, toolName, message, scopePath = null) {
|
|
|
299
175
|
}
|
|
300
176
|
function runtimeAuthorityForDirectWrite(input, toolName, seams, scopePath) {
|
|
301
177
|
const projectRoot = resolve(input.projectRoot);
|
|
302
|
-
|
|
178
|
+
// Project policy load failure: treat as disabled project layer, still apply
|
|
179
|
+
// independent story file_scope when present (#516 / #2443 Greptile P1).
|
|
180
|
+
let basePolicy = DEFAULT_RUNTIME_AUTHORITY_POLICY;
|
|
303
181
|
try {
|
|
304
|
-
|
|
182
|
+
basePolicy = (seams.loadRuntimeAuthority ?? loadRuntimeAuthorityFromProject)(projectRoot);
|
|
305
183
|
}
|
|
306
184
|
catch {
|
|
307
|
-
|
|
308
|
-
|
|
185
|
+
basePolicy = DEFAULT_RUNTIME_AUTHORITY_POLICY;
|
|
186
|
+
}
|
|
187
|
+
// Wave 3 unified write fence: intersect project runtimeAuthority with active
|
|
188
|
+
// story file_scope (#516 / #2443 / #2948). Single evaluation SoT via
|
|
189
|
+
// evaluateRuntimeAuthorityDirectWrite — no parallel writeScope engine.
|
|
190
|
+
let storyFence;
|
|
191
|
+
try {
|
|
192
|
+
storyFence = seams.loadStoryWriteFence
|
|
193
|
+
? seams.loadStoryWriteFence(projectRoot, scopePath)
|
|
194
|
+
: loadStoryWriteFenceFromPath(scopePath);
|
|
309
195
|
}
|
|
196
|
+
catch {
|
|
197
|
+
// Residual: host cannot load active story — project fence still applies.
|
|
198
|
+
storyFence = { fileScope: [], denyPaths: [] };
|
|
199
|
+
}
|
|
200
|
+
const fence = resolveWriteFence(basePolicy, storyFence.fileScope, {
|
|
201
|
+
storyDenyPaths: storyFence.denyPaths,
|
|
202
|
+
});
|
|
203
|
+
// Neither layer active → allow (same as disabled runtimeAuthority historically).
|
|
204
|
+
if (!fence.fenceActive)
|
|
205
|
+
return null;
|
|
310
206
|
const writeTarget = hookWriteTargetPath(input.payload);
|
|
311
207
|
const relPath = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
|
|
312
|
-
const verdict = evaluateRuntimeAuthorityDirectWrite({
|
|
208
|
+
const verdict = evaluateRuntimeAuthorityDirectWrite({
|
|
209
|
+
policy: fence.policy,
|
|
210
|
+
relPathPosix: relPath,
|
|
211
|
+
});
|
|
313
212
|
if (verdict.allowed)
|
|
314
213
|
return null;
|
|
315
|
-
return deny(input, verdict.code ?? "runtime-policy-deny-path", toolName, verdict.reason ?? "Directive denied this direct write under
|
|
214
|
+
return deny(input, verdict.code ?? "runtime-policy-deny-path", toolName, verdict.reason ?? "Directive denied this direct write under write fence policy.", scopePath);
|
|
215
|
+
}
|
|
216
|
+
function loadAuthzContext(projectRoot, seams) {
|
|
217
|
+
if (seams.loadAuthzState !== undefined) {
|
|
218
|
+
const state = seams.loadAuthzState(projectRoot);
|
|
219
|
+
const raw = (seams.loadAuthzGrants ?? listActiveHumanGrants)(projectRoot, state);
|
|
220
|
+
// Production filter: self-authored grants never enter the implement gate (#2944).
|
|
221
|
+
const grants = raw.filter((g) => evidenceSatisfiesImplementationApproval({ grant: g }));
|
|
222
|
+
return { state, grants, corrupt: false, corruptReason: null };
|
|
223
|
+
}
|
|
224
|
+
const loaded = loadAuthzStateResult(projectRoot);
|
|
225
|
+
const raw = (seams.loadAuthzGrants ?? listActiveHumanGrants)(projectRoot, loaded.state);
|
|
226
|
+
const grants = raw.filter((g) => evidenceSatisfiesImplementationApproval({ grant: g }));
|
|
227
|
+
return {
|
|
228
|
+
state: loaded.state,
|
|
229
|
+
grants,
|
|
230
|
+
corrupt: loaded.corrupt,
|
|
231
|
+
corruptReason: loaded.ok ? null : loaded.reason,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function authzCodeToHook(code) {
|
|
235
|
+
// Map authz decision codes onto HookDecisionCode (same string values for denials).
|
|
236
|
+
if (code === "authz-uat-deny" ||
|
|
237
|
+
code === "authz-grant-missing" ||
|
|
238
|
+
code === "authz-grant-origin-reject" ||
|
|
239
|
+
code === "authz-grant-scope-deny" ||
|
|
240
|
+
code === "authz-grant-expired" ||
|
|
241
|
+
code === "authz-grant-revoked" ||
|
|
242
|
+
code === "authz-grant-single-use-spent") {
|
|
243
|
+
return code;
|
|
244
|
+
}
|
|
245
|
+
return "authz-uat-deny";
|
|
246
|
+
}
|
|
247
|
+
function recordAuthzAudit(projectRoot, decision, state, seams) {
|
|
248
|
+
if (seams.authzAudit === false)
|
|
249
|
+
return;
|
|
250
|
+
// Only audit when UAT is active or a deny occurred under an authz code.
|
|
251
|
+
const uatActive = state.uat?.active === true;
|
|
252
|
+
if (!uatActive && decision.allowed)
|
|
253
|
+
return;
|
|
254
|
+
try {
|
|
255
|
+
appendAuthzAudit(projectRoot, {
|
|
256
|
+
schemaVersion: 1,
|
|
257
|
+
ts: utcIso(),
|
|
258
|
+
humanApprovalRef: decision.humanApprovalRef,
|
|
259
|
+
approvedScope: decision.approvedScope,
|
|
260
|
+
attemptedOp: String(decision.attemptedOp),
|
|
261
|
+
path: decision.path,
|
|
262
|
+
result: decision.allowed ? "allow" : "deny",
|
|
263
|
+
code: decision.code,
|
|
264
|
+
message: decision.reason,
|
|
265
|
+
campaignId: state.uat?.campaignId ?? null,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Audit write must not crash the host tool gate.
|
|
270
|
+
}
|
|
316
271
|
}
|
|
317
272
|
/**
|
|
318
|
-
*
|
|
319
|
-
*
|
|
273
|
+
* UAT lease + human-origin grant gate (#2944). Returns deny decision or null when allowed.
|
|
274
|
+
* Composes before runtimeAuthority path/scope checks for direct writes and shell/MCP.
|
|
320
275
|
*/
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
276
|
+
function authzForMutation(input, toolName, seams, options) {
|
|
277
|
+
const projectRoot = resolve(input.projectRoot);
|
|
278
|
+
let state;
|
|
279
|
+
let grants;
|
|
280
|
+
let corrupt;
|
|
281
|
+
let corruptReason;
|
|
282
|
+
try {
|
|
283
|
+
({ state, grants, corrupt, corruptReason } = loadAuthzContext(projectRoot, seams));
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
// Fail closed: unreadable authz store must not disable UAT enforcement (#2944).
|
|
287
|
+
return deny(input, "authz-uat-deny", toolName, `Directive denied this mutation: authz store unreadable (${String(err)}). ` +
|
|
288
|
+
"Human action required: repair `.deft/authz/state.json` or re-run `deft authz:uat-start`.", options.scopePath);
|
|
289
|
+
}
|
|
290
|
+
const shellCommand = isShellTool(toolName) ? hookShellCommand(input.payload) : null;
|
|
291
|
+
const ops = classifyHookAuthzOps({
|
|
292
|
+
toolName,
|
|
293
|
+
shellCommand,
|
|
294
|
+
isDirectWrite: options.isDirectWrite,
|
|
295
|
+
mcpArgsText: options.isDirectWrite ? null : hookMcpArgsText(input.payload),
|
|
296
|
+
});
|
|
297
|
+
// No classifiable authz ops (unrelated tools) — leave to other gates,
|
|
298
|
+
// except corrupt state still fails closed on direct writes / classifiable shell.
|
|
299
|
+
if (ops.length === 0) {
|
|
300
|
+
if (corrupt && (options.isDirectWrite || isShellTool(toolName))) {
|
|
301
|
+
return deny(input, "authz-uat-deny", toolName, `Directive denied this mutation: ${corruptReason ?? "authz state corrupt"}. ` +
|
|
302
|
+
"Fail closed while UAT authority cannot be verified. " +
|
|
303
|
+
"Human action required: repair `.deft/authz/state.json`.", options.scopePath);
|
|
304
|
+
}
|
|
341
305
|
return null;
|
|
306
|
+
}
|
|
307
|
+
// Structural context for bound grants (branch from git; repo optional env).
|
|
308
|
+
let branch = null;
|
|
342
309
|
try {
|
|
343
|
-
|
|
310
|
+
branch = detectBranch(projectRoot);
|
|
344
311
|
}
|
|
345
312
|
catch {
|
|
346
|
-
|
|
313
|
+
branch = null;
|
|
347
314
|
}
|
|
315
|
+
const repo = (process.env.DEFT_AUTHZ_REPO ?? process.env.GITHUB_REPOSITORY ?? "").trim() || null;
|
|
316
|
+
// Evaluate each op; first deny wins (compound shell must not short-circuit).
|
|
317
|
+
for (const op of ops) {
|
|
318
|
+
const decision = evaluateAuthzMutation({
|
|
319
|
+
state,
|
|
320
|
+
grants,
|
|
321
|
+
op,
|
|
322
|
+
path: options.relPath,
|
|
323
|
+
branch,
|
|
324
|
+
repo,
|
|
325
|
+
worktree: projectRoot,
|
|
326
|
+
});
|
|
327
|
+
recordAuthzAudit(projectRoot, decision, state, seams);
|
|
328
|
+
if (!decision.allowed) {
|
|
329
|
+
return deny(input, authzCodeToHook(decision.code), toolName, decision.reason, options.scopePath);
|
|
330
|
+
}
|
|
331
|
+
// Consume single-use grants after allow (persist usedAt).
|
|
332
|
+
if (shouldConsumeSingleUseGrant(decision) && decision.humanApprovalRef !== null) {
|
|
333
|
+
try {
|
|
334
|
+
markGrantUsed(projectRoot, decision.humanApprovalRef);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// Persistence failure must not flip allow → deny after a successful check;
|
|
338
|
+
// next load will still see unused if write failed (prefer allow-once risk over deadlock).
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
348
343
|
}
|
|
349
344
|
function loadRuntimeAuthorityPolicySafe(input, seams) {
|
|
350
345
|
const projectRoot = resolve(input.projectRoot);
|
|
@@ -361,6 +356,35 @@ function loadRuntimeAuthorityPolicySafe(input, seams) {
|
|
|
361
356
|
*/
|
|
362
357
|
function decideShellOrMcpRuntimeAuthority(input, toolName, seams) {
|
|
363
358
|
const projectRoot = resolve(input.projectRoot);
|
|
359
|
+
// Wave 1 authz first: UAT lease denies push/PR/merge/settings/deploy without cohort (#2944).
|
|
360
|
+
// Composes with #2711 shell matchers; does not re-implement them.
|
|
361
|
+
const authzDeny = authzForMutation(input, toolName, seams, {
|
|
362
|
+
isDirectWrite: false,
|
|
363
|
+
relPath: null,
|
|
364
|
+
scopePath: null,
|
|
365
|
+
});
|
|
366
|
+
if (authzDeny !== null)
|
|
367
|
+
return authzDeny;
|
|
368
|
+
// #1193 intent ceiling: non-implement slash verbs cannot authorize push/merge (and deploy via merge).
|
|
369
|
+
const env = input.environ ?? process.env;
|
|
370
|
+
const shellOpsPreview = [];
|
|
371
|
+
if (isShellTool(toolName)) {
|
|
372
|
+
const command = hookShellCommand(input.payload);
|
|
373
|
+
if (command !== null)
|
|
374
|
+
shellOpsPreview.push(...listShellOps(command));
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
const mcpOp = classifyMcpTool(toolName, hookMcpArgsText(input.payload));
|
|
378
|
+
if (mcpOp !== null)
|
|
379
|
+
shellOpsPreview.push(mcpOp);
|
|
380
|
+
}
|
|
381
|
+
for (const shellOp of shellOpsPreview) {
|
|
382
|
+
const intentOp = shellOp === "push" ? "push" : "merge";
|
|
383
|
+
const intent = evaluateIntentCeilingFromEnv(intentOp, env);
|
|
384
|
+
if (!intent.allowed) {
|
|
385
|
+
return deny(input, "intent-ceiling-deny", toolName, intent.reason);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
364
388
|
const policy = loadRuntimeAuthorityPolicySafe(input, seams);
|
|
365
389
|
if (policy === null) {
|
|
366
390
|
return {
|
|
@@ -429,17 +453,40 @@ function inspectMutationGates(input, toolName, seams, options) {
|
|
|
429
453
|
((root) => inspectSessionRitual(root, { tier: "gated", posture: "mutation" })))(projectRoot);
|
|
430
454
|
}
|
|
431
455
|
catch (cause) {
|
|
456
|
+
// #2994: best-effort local process-cost; never changes deny verdict.
|
|
457
|
+
emitSessionRitualBlockedProcessCost({
|
|
458
|
+
toolName,
|
|
459
|
+
code: "ritual-not-ready",
|
|
460
|
+
recoveryTier: "cold",
|
|
461
|
+
detail: `inspect threw: ${String(cause)}`,
|
|
462
|
+
}, { projectRoot });
|
|
432
463
|
return deny(input, "ritual-not-ready", toolName, `Directive could not inspect the gated session ritual: ${String(cause)}. ` +
|
|
433
|
-
"
|
|
464
|
+
formatRitualRecoveryInstruction("cold"));
|
|
434
465
|
}
|
|
435
466
|
if (ritual.code !== 0) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
467
|
+
// #2992: prefer re-arm recovery when age/compact stale; cold when bind invalid.
|
|
468
|
+
const recoveryTier = ritual.recoveryTier === "rearm" ? "rearm" : "cold";
|
|
469
|
+
// #2994: best-effort local process-cost; never changes deny verdict.
|
|
470
|
+
emitSessionRitualBlockedProcessCost({
|
|
471
|
+
toolName,
|
|
472
|
+
code: "ritual-not-ready",
|
|
473
|
+
recoveryTier,
|
|
474
|
+
detail: ritual.message,
|
|
475
|
+
}, { projectRoot });
|
|
476
|
+
return deny(input, "ritual-not-ready", toolName, `Directive denied ${toolName}: ${ritual.message} ${formatRitualRecoveryInstruction(recoveryTier)}`);
|
|
439
477
|
}
|
|
440
478
|
if (options.proposedLifecycleExempt) {
|
|
441
479
|
const writeTarget = hookWriteTargetPath(input.payload);
|
|
442
480
|
if (isProposedLifecycleWrite(projectRoot, writeTarget)) {
|
|
481
|
+
const relPath = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
|
|
482
|
+
// UAT still allows xbrief/proposed/** as evidence/defect capture (#2944).
|
|
483
|
+
const authzDeny = authzForMutation(input, toolName, seams, {
|
|
484
|
+
isDirectWrite: true,
|
|
485
|
+
relPath,
|
|
486
|
+
scopePath: null,
|
|
487
|
+
});
|
|
488
|
+
if (authzDeny !== null)
|
|
489
|
+
return authzDeny;
|
|
443
490
|
const runtimeDeny = runtimeAuthorityForDirectWrite(input, toolName, seams, null);
|
|
444
491
|
if (runtimeDeny !== null)
|
|
445
492
|
return runtimeDeny;
|
|
@@ -487,7 +534,24 @@ function inspectMutationGates(input, toolName, seams, options) {
|
|
|
487
534
|
}
|
|
488
535
|
}
|
|
489
536
|
const allowCode = isSpawnTool(toolName) ? "spawn-ready" : "write-ready";
|
|
537
|
+
// #1193: non-implement slash verbs must not authorize implement/spawn tooling.
|
|
538
|
+
{
|
|
539
|
+
const intent = evaluateIntentCeilingFromEnv("implement", input.environ ?? process.env);
|
|
540
|
+
if (!intent.allowed) {
|
|
541
|
+
return deny(input, "intent-ceiling-deny", toolName, intent.reason);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
490
544
|
if (!isSpawnTool(toolName)) {
|
|
545
|
+
const writeTarget = hookWriteTargetPath(input.payload);
|
|
546
|
+
const relPath = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
|
|
547
|
+
// Wave 1 authz (UAT lease + human-origin grant) before runtimeAuthority (#2944 / #2948 L1–L2).
|
|
548
|
+
const authzDeny = authzForMutation(input, toolName, seams, {
|
|
549
|
+
isDirectWrite: true,
|
|
550
|
+
relPath,
|
|
551
|
+
scopePath: scope.path,
|
|
552
|
+
});
|
|
553
|
+
if (authzDeny !== null)
|
|
554
|
+
return authzDeny;
|
|
491
555
|
const runtimeDeny = runtimeAuthorityForDirectWrite(input, toolName, seams, scope.path);
|
|
492
556
|
if (runtimeDeny !== null)
|
|
493
557
|
return runtimeDeny;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared host × OS × tool fixture corpus (#2950 Phase A skeleton, Phase B matrix expansion).
|
|
3
|
+
*
|
|
4
|
+
* Cases are pure data: raw stdin or structured payload → expected classification.
|
|
5
|
+
* Core tests and CLI hook-dispatch tests import this module (shared corpus collapse).
|
|
6
|
+
*/
|
|
7
|
+
import type { ClassifyHookHost, HookWriteIntent } from "../classify/types.js";
|
|
8
|
+
export type FixtureOs = "win32" | "posix";
|
|
9
|
+
export type FixtureToolFamily = "Write" | "ApplyPatch" | "Task" | "StrReplace" | "other";
|
|
10
|
+
export interface HookFixtureCase {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly host: ClassifyHookHost;
|
|
13
|
+
readonly os: FixtureOs;
|
|
14
|
+
readonly tool: FixtureToolFamily;
|
|
15
|
+
/** Closed-issue tags for the regression class this case freezes. */
|
|
16
|
+
readonly regression: readonly string[];
|
|
17
|
+
/** Structured payload when the host sends JSON. */
|
|
18
|
+
readonly payload?: unknown;
|
|
19
|
+
/** Raw stdin when testing BOM / free-form ApplyPatch parse. */
|
|
20
|
+
readonly raw?: string;
|
|
21
|
+
readonly expected: {
|
|
22
|
+
readonly toolName: string | null;
|
|
23
|
+
readonly writeIntent: HookWriteIntent;
|
|
24
|
+
readonly writeTargetPath: string | null;
|
|
25
|
+
/** When raw is set: after parseHookStdin. */
|
|
26
|
+
readonly stdinEmpty?: boolean;
|
|
27
|
+
readonly parseFailed?: boolean;
|
|
28
|
+
};
|
|
29
|
+
readonly notes?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Cursor × {win32, posix} × {Write, ApplyPatch, Task, StrReplace}
|
|
33
|
+
* plus recent regression classes (BOM, free-form, missing tool name, outside-root).
|
|
34
|
+
* Phase B expands Write/ApplyPatch depth and shares this corpus with CLI tests.
|
|
35
|
+
*/
|
|
36
|
+
export declare const HOOK_FIXTURE_CASES: readonly HookFixtureCase[];
|
|
37
|
+
export declare function fixtureCasesFor(filter: {
|
|
38
|
+
host?: ClassifyHookHost;
|
|
39
|
+
os?: FixtureOs;
|
|
40
|
+
tool?: FixtureToolFamily;
|
|
41
|
+
}): HookFixtureCase[];
|
|
42
|
+
/** Look up a single case by stable id (CLI/core shared helpers). */
|
|
43
|
+
export declare function fixtureCaseById(id: string): HookFixtureCase | undefined;
|
|
44
|
+
//# sourceMappingURL=cases.d.ts.map
|