@deftai/directive-core 0.87.0 → 0.89.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/scanner.d.ts +11 -1
- package/dist/cache/scanner.js +29 -4
- 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/check/gate-lists.js +1 -0
- package/dist/codebase/default-extractor.js +3 -0
- package/dist/codebase/map.js +11 -4
- package/dist/content-contracts/skills/helpers.d.ts +10 -0
- package/dist/content-contracts/skills/helpers.js +35 -0
- package/dist/deposit/copy-tree.d.ts +19 -1
- package/dist/deposit/copy-tree.js +134 -5
- package/dist/doctor/doctor-state.js +28 -4
- package/dist/doctor/main.d.ts +10 -0
- package/dist/doctor/main.js +208 -0
- package/dist/doctor/types.d.ts +11 -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/fs/projection-containment.d.ts +18 -0
- package/dist/fs/projection-containment.js +40 -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 +30 -15
- package/dist/hooks/dispatcher.js +326 -134
- 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/hooks/tools.d.ts +31 -0
- package/dist/hooks/tools.js +74 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +65 -10
- package/dist/init-deposit/gitignore.js +11 -3
- package/dist/init-deposit/headless-manifest.js +12 -4
- package/dist/init-deposit/hygiene.d.ts +16 -0
- package/dist/init-deposit/hygiene.js +26 -0
- package/dist/init-deposit/init-dispatch.js +28 -0
- package/dist/init-deposit/migrate.d.ts +1 -1
- package/dist/init-deposit/migrate.js +13 -3
- package/dist/init-deposit/prettierignore.js +12 -5
- package/dist/init-deposit/refresh.js +38 -7
- package/dist/init-deposit/scaffold.js +42 -29
- package/dist/init-deposit/xbrief-projections.js +17 -10
- 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/intake/issue-ingest.js +11 -2
- package/dist/lifecycle/event-detect.js +12 -4
- package/dist/lifecycle/events.js +29 -20
- package/dist/lifecycle/lifecycle-hygiene.js +9 -3
- package/dist/packs/pack-render.d.ts +33 -0
- package/dist/packs/pack-render.js +172 -13
- package/dist/packs/quarantine-ext.d.ts +10 -0
- package/dist/packs/quarantine-ext.js +26 -2
- 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 +5 -0
- package/dist/policy/index.js +32 -1
- package/dist/policy/intent-ceiling.d.ts +79 -0
- package/dist/policy/intent-ceiling.js +181 -0
- package/dist/policy/no-deft-directive.d.ts +59 -0
- package/dist/policy/no-deft-directive.js +110 -0
- package/dist/policy/org-force-on-migration.d.ts +52 -0
- package/dist/policy/org-force-on-migration.js +269 -27
- 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 +56 -2
- package/dist/policy/runtime-authority.js +297 -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-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/release-availability.js +26 -6
- package/dist/session/ritual-sentinel.js +19 -12
- package/dist/session/session-start-hook.d.ts +3 -0
- package/dist/session/session-start-hook.js +15 -0
- package/dist/session/session-start.js +37 -0
- 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/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/cursor-tier1.js +7 -2
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/openclaw-tier1.js +19 -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 +16 -3
package/dist/hooks/dispatcher.js
CHANGED
|
@@ -1,145 +1,29 @@
|
|
|
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";
|
|
4
|
-
import {
|
|
5
|
+
import { evaluateIntentCeilingFromEnv } from "../policy/intent-ceiling.js";
|
|
6
|
+
import { detectNoDeftDirective, NO_DEFT_DIRECTIVE_DISABLED_MESSAGE, NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE, } from "../policy/no-deft-directive.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";
|
|
5
10
|
import { markRitualStaleAfterCompact } from "../session/ritual-sentinel.js";
|
|
6
11
|
import { runSessionStartHookWrite } from "../session/session-start-hook.js";
|
|
7
12
|
import { inspectSessionRitual } from "../session/verify-session-ritual.js";
|
|
13
|
+
import { hookMcpArgsText, hookShellCommand, hookToolName, hookWriteTargetPath, missingToolNameMessage, record, } from "./classify/index.js";
|
|
8
14
|
import { isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
|
|
9
15
|
import { inspectActiveScope } from "./scope.js";
|
|
10
|
-
import { isDirectWriteTool, isSpawnTool } from "./tools.js";
|
|
16
|
+
import { isDirectWriteTool, isMcpTool, isShellTool, isSpawnTool } from "./tools.js";
|
|
17
|
+
// Pure parse/classify helpers are defined in ./classify/ and re-exported from
|
|
18
|
+
// ./index.ts (#2950). Dispatcher is orchestration: classify → policy → decision.
|
|
11
19
|
export { hookReadOnlyFromPayload, isExploreSpawn, isReadOnlyHookContext } from "./readonly.js";
|
|
12
|
-
export { DIRECT_WRITE_HOOK_MATCHER, DIRECT_WRITE_TOOL_NAMES, isDirectWriteTool, isSpawnTool, READ_ONLY_HOOK_ENV, SPAWN_HOOK_MATCHER, SPAWN_TOOL_NAMES, } from "./tools.js";
|
|
20
|
+
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";
|
|
13
21
|
export const HOOK_HOSTS = ["claude", "grok", "cursor", "codex"];
|
|
14
22
|
export const HOOK_EVENTS = ["session.start", "session.compact", "tool.before"];
|
|
15
23
|
/** Hosts that receive compact/resume hook deposits via init/update (#2113). */
|
|
16
24
|
export const COMPACT_HOOK_HOSTS = ["claude", "grok", "cursor"];
|
|
17
25
|
/** Hosts without a native compact hook surface — deposit skips cleanly (#2113). */
|
|
18
26
|
export const COMPACT_HOOK_SKIP_HOSTS = ["codex"];
|
|
19
|
-
function record(value) {
|
|
20
|
-
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
21
|
-
? value
|
|
22
|
-
: null;
|
|
23
|
-
}
|
|
24
|
-
function firstString(...values) {
|
|
25
|
-
for (const value of values) {
|
|
26
|
-
if (typeof value === "string" && value.trim().length > 0)
|
|
27
|
-
return value.trim();
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
function fieldPresent(input, key) {
|
|
32
|
-
return key in input;
|
|
33
|
-
}
|
|
34
|
-
function fieldString(input, key) {
|
|
35
|
-
const value = input[key];
|
|
36
|
-
if (typeof value === "string" && value.trim().length > 0)
|
|
37
|
-
return value.trim();
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
function toolInputRecord(payload) {
|
|
41
|
-
const toolCall = record(payload.tool_call) ?? record(payload.toolCall);
|
|
42
|
-
return (record(payload.tool_input) ??
|
|
43
|
-
record(payload.toolInput) ??
|
|
44
|
-
record(payload.input) ??
|
|
45
|
-
record(payload.arguments) ??
|
|
46
|
-
(toolCall !== null ? record(toolCall.arguments) : null));
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Cursor preToolUse payloads sometimes omit `tool_name` even when the hook matcher
|
|
50
|
-
* fired for a direct-write tool (#2628). Infer from nested tool input when possible.
|
|
51
|
-
*/
|
|
52
|
-
function inferCursorDirectWriteToolName(payload) {
|
|
53
|
-
const toolInput = toolInputRecord(payload);
|
|
54
|
-
if (toolInput !== null) {
|
|
55
|
-
if (fieldPresent(toolInput, "new_string") ||
|
|
56
|
-
fieldPresent(toolInput, "newString") ||
|
|
57
|
-
fieldPresent(toolInput, "old_string") ||
|
|
58
|
-
fieldPresent(toolInput, "oldString")) {
|
|
59
|
-
return "StrReplace";
|
|
60
|
-
}
|
|
61
|
-
if (fieldString(toolInput, "contents") !== null ||
|
|
62
|
-
fieldString(toolInput, "content") !== null ||
|
|
63
|
-
fieldString(toolInput, "text") !== null) {
|
|
64
|
-
return "Write";
|
|
65
|
-
}
|
|
66
|
-
if (fieldString(toolInput, "patch") !== null ||
|
|
67
|
-
fieldString(toolInput, "unified_diff") !== null ||
|
|
68
|
-
fieldString(toolInput, "diff") !== null) {
|
|
69
|
-
return "ApplyPatch";
|
|
70
|
-
}
|
|
71
|
-
if (Array.isArray(toolInput.edits))
|
|
72
|
-
return "MultiEdit";
|
|
73
|
-
if (Array.isArray(toolInput.cells) || toolInput.cell_id != null) {
|
|
74
|
-
return "NotebookEdit";
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
// Cursor maps Claude Edit → Write; a write target without contents is still a direct write.
|
|
78
|
-
if (hookWriteTargetPath(payload) !== null)
|
|
79
|
-
return "Write";
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
export function hookPayloadTopLevelKeys(payload) {
|
|
83
|
-
const input = record(payload);
|
|
84
|
-
if (input === null)
|
|
85
|
-
return [];
|
|
86
|
-
return Object.keys(input).sort();
|
|
87
|
-
}
|
|
88
|
-
export function hookToolName(payload, host) {
|
|
89
|
-
const input = record(payload);
|
|
90
|
-
if (input === null)
|
|
91
|
-
return null;
|
|
92
|
-
const toolObject = record(input.tool);
|
|
93
|
-
const toolCall = record(input.tool_call) ?? record(input.toolCall);
|
|
94
|
-
// OpenAI-style nestings are host-agnostic; checked before Cursor-only inference.
|
|
95
|
-
const direct = fieldString(input, "tool_name") ??
|
|
96
|
-
fieldString(input, "toolName") ??
|
|
97
|
-
fieldString(input, "tool") ??
|
|
98
|
-
(toolObject !== null ? fieldString(toolObject, "name") : null) ??
|
|
99
|
-
(toolCall !== null ? fieldString(toolCall, "name") : null);
|
|
100
|
-
if (direct !== null)
|
|
101
|
-
return direct;
|
|
102
|
-
if (host === "cursor")
|
|
103
|
-
return inferCursorDirectWriteToolName(input);
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
function missingToolNameMessage(input) {
|
|
107
|
-
const { host, payload, context } = input;
|
|
108
|
-
if (host === "cursor") {
|
|
109
|
-
if (context?.stdinEmpty) {
|
|
110
|
-
return ("Directive denied this Cursor preToolUse event because the host sent an empty payload " +
|
|
111
|
-
"(stdin was empty — not a session ritual or scope failure). " +
|
|
112
|
-
"If write tools should pass, update Directive or report the payload shape from Cursor.");
|
|
113
|
-
}
|
|
114
|
-
if (context?.parseFailed) {
|
|
115
|
-
return ("Directive denied this Cursor preToolUse event because the host payload was not valid JSON " +
|
|
116
|
-
"(host-integration mismatch — not a session ritual or scope failure). " +
|
|
117
|
-
"If write tools should pass, update Directive or report the payload shape from Cursor.");
|
|
118
|
-
}
|
|
119
|
-
const keys = hookPayloadTopLevelKeys(payload);
|
|
120
|
-
if (keys.length > 0) {
|
|
121
|
-
return ("Directive denied this Cursor preToolUse event because the host payload omitted a " +
|
|
122
|
-
"recognizable tool name (host-integration mismatch — not a session ritual or scope failure). " +
|
|
123
|
-
`Top-level payload keys: ${keys.join(", ")}. ` +
|
|
124
|
-
"If write tools should pass, update Directive or report the payload shape from Cursor.");
|
|
125
|
-
}
|
|
126
|
-
return ("Directive denied this Cursor preToolUse event because the host payload omitted a " +
|
|
127
|
-
"recognizable tool name (host-integration mismatch — not a session ritual or scope failure). " +
|
|
128
|
-
"If write tools should pass, update Directive or report the payload shape from Cursor.");
|
|
129
|
-
}
|
|
130
|
-
return "Directive denied this matched write event because the host payload omitted its tool name.";
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Best-effort write-target path from host PreToolUse payloads (#2625).
|
|
134
|
-
* Hosts disagree on nesting (`tool_input.file_path` vs top-level `path`).
|
|
135
|
-
*/
|
|
136
|
-
export function hookWriteTargetPath(payload) {
|
|
137
|
-
const input = record(payload);
|
|
138
|
-
if (input === null)
|
|
139
|
-
return null;
|
|
140
|
-
const toolInput = toolInputRecord(input);
|
|
141
|
-
return firstString(toolInput?.file_path, toolInput?.filePath, toolInput?.path, input.file_path, input.filePath, input.path);
|
|
142
|
-
}
|
|
143
27
|
/** POSIX-ish project-relative path for lifecycle matching. */
|
|
144
28
|
export function toProjectRelativePosix(projectRoot, targetPath) {
|
|
145
29
|
const abs = resolve(projectRoot, targetPath.replace(/\\/g, "/"));
|
|
@@ -290,20 +174,275 @@ function deny(input, code, toolName, message, scopePath = null) {
|
|
|
290
174
|
}
|
|
291
175
|
function runtimeAuthorityForDirectWrite(input, toolName, seams, scopePath) {
|
|
292
176
|
const projectRoot = resolve(input.projectRoot);
|
|
293
|
-
|
|
177
|
+
// Project policy load failure: treat as disabled project layer, still apply
|
|
178
|
+
// independent story file_scope when present (#516 / #2443 Greptile P1).
|
|
179
|
+
let basePolicy = DEFAULT_RUNTIME_AUTHORITY_POLICY;
|
|
294
180
|
try {
|
|
295
|
-
|
|
181
|
+
basePolicy = (seams.loadRuntimeAuthority ?? loadRuntimeAuthorityFromProject)(projectRoot);
|
|
296
182
|
}
|
|
297
183
|
catch {
|
|
298
|
-
|
|
299
|
-
|
|
184
|
+
basePolicy = DEFAULT_RUNTIME_AUTHORITY_POLICY;
|
|
185
|
+
}
|
|
186
|
+
// Wave 3 unified write fence: intersect project runtimeAuthority with active
|
|
187
|
+
// story file_scope (#516 / #2443 / #2948). Single evaluation SoT via
|
|
188
|
+
// evaluateRuntimeAuthorityDirectWrite — no parallel writeScope engine.
|
|
189
|
+
let storyFence;
|
|
190
|
+
try {
|
|
191
|
+
storyFence = seams.loadStoryWriteFence
|
|
192
|
+
? seams.loadStoryWriteFence(projectRoot, scopePath)
|
|
193
|
+
: loadStoryWriteFenceFromPath(scopePath);
|
|
300
194
|
}
|
|
195
|
+
catch {
|
|
196
|
+
// Residual: host cannot load active story — project fence still applies.
|
|
197
|
+
storyFence = { fileScope: [], denyPaths: [] };
|
|
198
|
+
}
|
|
199
|
+
const fence = resolveWriteFence(basePolicy, storyFence.fileScope, {
|
|
200
|
+
storyDenyPaths: storyFence.denyPaths,
|
|
201
|
+
});
|
|
202
|
+
// Neither layer active → allow (same as disabled runtimeAuthority historically).
|
|
203
|
+
if (!fence.fenceActive)
|
|
204
|
+
return null;
|
|
301
205
|
const writeTarget = hookWriteTargetPath(input.payload);
|
|
302
206
|
const relPath = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
|
|
303
|
-
const verdict = evaluateRuntimeAuthorityDirectWrite({
|
|
207
|
+
const verdict = evaluateRuntimeAuthorityDirectWrite({
|
|
208
|
+
policy: fence.policy,
|
|
209
|
+
relPathPosix: relPath,
|
|
210
|
+
});
|
|
304
211
|
if (verdict.allowed)
|
|
305
212
|
return null;
|
|
306
|
-
return deny(input, verdict.code ?? "runtime-policy-deny-path", toolName, verdict.reason ?? "Directive denied this direct write under
|
|
213
|
+
return deny(input, verdict.code ?? "runtime-policy-deny-path", toolName, verdict.reason ?? "Directive denied this direct write under write fence policy.", scopePath);
|
|
214
|
+
}
|
|
215
|
+
function loadAuthzContext(projectRoot, seams) {
|
|
216
|
+
if (seams.loadAuthzState !== undefined) {
|
|
217
|
+
const state = seams.loadAuthzState(projectRoot);
|
|
218
|
+
const raw = (seams.loadAuthzGrants ?? listActiveHumanGrants)(projectRoot, state);
|
|
219
|
+
// Production filter: self-authored grants never enter the implement gate (#2944).
|
|
220
|
+
const grants = raw.filter((g) => evidenceSatisfiesImplementationApproval({ grant: g }));
|
|
221
|
+
return { state, grants, corrupt: false, corruptReason: null };
|
|
222
|
+
}
|
|
223
|
+
const loaded = loadAuthzStateResult(projectRoot);
|
|
224
|
+
const raw = (seams.loadAuthzGrants ?? listActiveHumanGrants)(projectRoot, loaded.state);
|
|
225
|
+
const grants = raw.filter((g) => evidenceSatisfiesImplementationApproval({ grant: g }));
|
|
226
|
+
return {
|
|
227
|
+
state: loaded.state,
|
|
228
|
+
grants,
|
|
229
|
+
corrupt: loaded.corrupt,
|
|
230
|
+
corruptReason: loaded.ok ? null : loaded.reason,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function authzCodeToHook(code) {
|
|
234
|
+
// Map authz decision codes onto HookDecisionCode (same string values for denials).
|
|
235
|
+
if (code === "authz-uat-deny" ||
|
|
236
|
+
code === "authz-grant-missing" ||
|
|
237
|
+
code === "authz-grant-origin-reject" ||
|
|
238
|
+
code === "authz-grant-scope-deny" ||
|
|
239
|
+
code === "authz-grant-expired" ||
|
|
240
|
+
code === "authz-grant-revoked" ||
|
|
241
|
+
code === "authz-grant-single-use-spent") {
|
|
242
|
+
return code;
|
|
243
|
+
}
|
|
244
|
+
return "authz-uat-deny";
|
|
245
|
+
}
|
|
246
|
+
function recordAuthzAudit(projectRoot, decision, state, seams) {
|
|
247
|
+
if (seams.authzAudit === false)
|
|
248
|
+
return;
|
|
249
|
+
// Only audit when UAT is active or a deny occurred under an authz code.
|
|
250
|
+
const uatActive = state.uat?.active === true;
|
|
251
|
+
if (!uatActive && decision.allowed)
|
|
252
|
+
return;
|
|
253
|
+
try {
|
|
254
|
+
appendAuthzAudit(projectRoot, {
|
|
255
|
+
schemaVersion: 1,
|
|
256
|
+
ts: utcIso(),
|
|
257
|
+
humanApprovalRef: decision.humanApprovalRef,
|
|
258
|
+
approvedScope: decision.approvedScope,
|
|
259
|
+
attemptedOp: String(decision.attemptedOp),
|
|
260
|
+
path: decision.path,
|
|
261
|
+
result: decision.allowed ? "allow" : "deny",
|
|
262
|
+
code: decision.code,
|
|
263
|
+
message: decision.reason,
|
|
264
|
+
campaignId: state.uat?.campaignId ?? null,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// Audit write must not crash the host tool gate.
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* UAT lease + human-origin grant gate (#2944). Returns deny decision or null when allowed.
|
|
273
|
+
* Composes before runtimeAuthority path/scope checks for direct writes and shell/MCP.
|
|
274
|
+
*/
|
|
275
|
+
function authzForMutation(input, toolName, seams, options) {
|
|
276
|
+
const projectRoot = resolve(input.projectRoot);
|
|
277
|
+
let state;
|
|
278
|
+
let grants;
|
|
279
|
+
let corrupt;
|
|
280
|
+
let corruptReason;
|
|
281
|
+
try {
|
|
282
|
+
({ state, grants, corrupt, corruptReason } = loadAuthzContext(projectRoot, seams));
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
// Fail closed: unreadable authz store must not disable UAT enforcement (#2944).
|
|
286
|
+
return deny(input, "authz-uat-deny", toolName, `Directive denied this mutation: authz store unreadable (${String(err)}). ` +
|
|
287
|
+
"Human action required: repair `.deft/authz/state.json` or re-run `deft authz:uat-start`.", options.scopePath);
|
|
288
|
+
}
|
|
289
|
+
const shellCommand = isShellTool(toolName) ? hookShellCommand(input.payload) : null;
|
|
290
|
+
const ops = classifyHookAuthzOps({
|
|
291
|
+
toolName,
|
|
292
|
+
shellCommand,
|
|
293
|
+
isDirectWrite: options.isDirectWrite,
|
|
294
|
+
mcpArgsText: options.isDirectWrite ? null : hookMcpArgsText(input.payload),
|
|
295
|
+
});
|
|
296
|
+
// No classifiable authz ops (unrelated tools) — leave to other gates,
|
|
297
|
+
// except corrupt state still fails closed on direct writes / classifiable shell.
|
|
298
|
+
if (ops.length === 0) {
|
|
299
|
+
if (corrupt && (options.isDirectWrite || isShellTool(toolName))) {
|
|
300
|
+
return deny(input, "authz-uat-deny", toolName, `Directive denied this mutation: ${corruptReason ?? "authz state corrupt"}. ` +
|
|
301
|
+
"Fail closed while UAT authority cannot be verified. " +
|
|
302
|
+
"Human action required: repair `.deft/authz/state.json`.", options.scopePath);
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
// Structural context for bound grants (branch from git; repo optional env).
|
|
307
|
+
let branch = null;
|
|
308
|
+
try {
|
|
309
|
+
branch = detectBranch(projectRoot);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
branch = null;
|
|
313
|
+
}
|
|
314
|
+
const repo = (process.env.DEFT_AUTHZ_REPO ?? process.env.GITHUB_REPOSITORY ?? "").trim() || null;
|
|
315
|
+
// Evaluate each op; first deny wins (compound shell must not short-circuit).
|
|
316
|
+
for (const op of ops) {
|
|
317
|
+
const decision = evaluateAuthzMutation({
|
|
318
|
+
state,
|
|
319
|
+
grants,
|
|
320
|
+
op,
|
|
321
|
+
path: options.relPath,
|
|
322
|
+
branch,
|
|
323
|
+
repo,
|
|
324
|
+
worktree: projectRoot,
|
|
325
|
+
});
|
|
326
|
+
recordAuthzAudit(projectRoot, decision, state, seams);
|
|
327
|
+
if (!decision.allowed) {
|
|
328
|
+
return deny(input, authzCodeToHook(decision.code), toolName, decision.reason, options.scopePath);
|
|
329
|
+
}
|
|
330
|
+
// Consume single-use grants after allow (persist usedAt).
|
|
331
|
+
if (shouldConsumeSingleUseGrant(decision) && decision.humanApprovalRef !== null) {
|
|
332
|
+
try {
|
|
333
|
+
markGrantUsed(projectRoot, decision.humanApprovalRef);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// Persistence failure must not flip allow → deny after a successful check;
|
|
337
|
+
// next load will still see unused if write failed (prefer allow-once risk over deadlock).
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
function loadRuntimeAuthorityPolicySafe(input, seams) {
|
|
344
|
+
const projectRoot = resolve(input.projectRoot);
|
|
345
|
+
try {
|
|
346
|
+
return (seams.loadRuntimeAuthority ?? loadRuntimeAuthorityFromProject)(projectRoot);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Evaluate scopes.push / scopes.merge for Shell/Bash or classifiable MCP tools (#2711).
|
|
354
|
+
* Returns a full HookDecision (allow or deny). Unclassifiable ops fail open.
|
|
355
|
+
*/
|
|
356
|
+
function decideShellOrMcpRuntimeAuthority(input, toolName, seams) {
|
|
357
|
+
const projectRoot = resolve(input.projectRoot);
|
|
358
|
+
// Wave 1 authz first: UAT lease denies push/PR/merge/settings/deploy without cohort (#2944).
|
|
359
|
+
// Composes with #2711 shell matchers; does not re-implement them.
|
|
360
|
+
const authzDeny = authzForMutation(input, toolName, seams, {
|
|
361
|
+
isDirectWrite: false,
|
|
362
|
+
relPath: null,
|
|
363
|
+
scopePath: null,
|
|
364
|
+
});
|
|
365
|
+
if (authzDeny !== null)
|
|
366
|
+
return authzDeny;
|
|
367
|
+
// #1193 intent ceiling: non-implement slash verbs cannot authorize push/merge (and deploy via merge).
|
|
368
|
+
const env = input.environ ?? process.env;
|
|
369
|
+
const shellOpsPreview = [];
|
|
370
|
+
if (isShellTool(toolName)) {
|
|
371
|
+
const command = hookShellCommand(input.payload);
|
|
372
|
+
if (command !== null)
|
|
373
|
+
shellOpsPreview.push(...listShellOps(command));
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
const mcpOp = classifyMcpTool(toolName, hookMcpArgsText(input.payload));
|
|
377
|
+
if (mcpOp !== null)
|
|
378
|
+
shellOpsPreview.push(mcpOp);
|
|
379
|
+
}
|
|
380
|
+
for (const shellOp of shellOpsPreview) {
|
|
381
|
+
const intentOp = shellOp === "push" ? "push" : "merge";
|
|
382
|
+
const intent = evaluateIntentCeilingFromEnv(intentOp, env);
|
|
383
|
+
if (!intent.allowed) {
|
|
384
|
+
return deny(input, "intent-ceiling-deny", toolName, intent.reason);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const policy = loadRuntimeAuthorityPolicySafe(input, seams);
|
|
388
|
+
if (policy === null) {
|
|
389
|
+
return {
|
|
390
|
+
verdict: "allow",
|
|
391
|
+
code: "shell-op-unclassifiable",
|
|
392
|
+
event: input.event,
|
|
393
|
+
host: input.host,
|
|
394
|
+
toolName,
|
|
395
|
+
projectRoot,
|
|
396
|
+
message: `Directive allowed ${toolName}: runtimeAuthority policy load failed (fail open).`,
|
|
397
|
+
scopePath: null,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
// Shell: evaluate every classifiable op in compound/multi-line commands (#2711 Greptile).
|
|
401
|
+
// MCP / bare push-merge names: single tool name maps to at most one op
|
|
402
|
+
// (includes bare `merge_pull_request` etc. that isMcpTool does not flag).
|
|
403
|
+
const ops = [];
|
|
404
|
+
if (isShellTool(toolName)) {
|
|
405
|
+
const command = hookShellCommand(input.payload);
|
|
406
|
+
if (command !== null)
|
|
407
|
+
ops.push(...listShellOps(command));
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
const mcpOp = classifyMcpTool(toolName, hookMcpArgsText(input.payload));
|
|
411
|
+
if (mcpOp !== null)
|
|
412
|
+
ops.push(mcpOp);
|
|
413
|
+
}
|
|
414
|
+
if (ops.length === 0) {
|
|
415
|
+
return {
|
|
416
|
+
verdict: "allow",
|
|
417
|
+
code: "shell-op-unclassifiable",
|
|
418
|
+
event: input.event,
|
|
419
|
+
host: input.host,
|
|
420
|
+
toolName,
|
|
421
|
+
projectRoot,
|
|
422
|
+
message: `${toolName} is classifiable as Shell/MCP but the command/tool was not recognized as ` +
|
|
423
|
+
"push or merge — fail open (host gap; see runtime-authority.md).",
|
|
424
|
+
scopePath: null,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
// Deny if any classifiable op is out of scope (compound commands must not short-circuit).
|
|
428
|
+
for (const op of ops) {
|
|
429
|
+
const verdict = evaluateRuntimeAuthorityShellOp({ policy, op });
|
|
430
|
+
if (!verdict.allowed) {
|
|
431
|
+
return deny(input, verdict.code ?? "runtime-policy-deny-scope", toolName, verdict.reason ??
|
|
432
|
+
"Directive denied this shell/MCP operation under runtime authority policy.");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
const opsLabel = ops.join("+");
|
|
436
|
+
return {
|
|
437
|
+
verdict: "allow",
|
|
438
|
+
code: "shell-op-ready",
|
|
439
|
+
event: input.event,
|
|
440
|
+
host: input.host,
|
|
441
|
+
toolName,
|
|
442
|
+
projectRoot,
|
|
443
|
+
message: `Directive runtimeAuthority allowed classifiable ${opsLabel} via ${toolName}.`,
|
|
444
|
+
scopePath: null,
|
|
445
|
+
};
|
|
307
446
|
}
|
|
308
447
|
function inspectMutationGates(input, toolName, seams, options) {
|
|
309
448
|
const projectRoot = resolve(input.projectRoot);
|
|
@@ -324,6 +463,15 @@ function inspectMutationGates(input, toolName, seams, options) {
|
|
|
324
463
|
if (options.proposedLifecycleExempt) {
|
|
325
464
|
const writeTarget = hookWriteTargetPath(input.payload);
|
|
326
465
|
if (isProposedLifecycleWrite(projectRoot, writeTarget)) {
|
|
466
|
+
const relPath = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
|
|
467
|
+
// UAT still allows xbrief/proposed/** as evidence/defect capture (#2944).
|
|
468
|
+
const authzDeny = authzForMutation(input, toolName, seams, {
|
|
469
|
+
isDirectWrite: true,
|
|
470
|
+
relPath,
|
|
471
|
+
scopePath: null,
|
|
472
|
+
});
|
|
473
|
+
if (authzDeny !== null)
|
|
474
|
+
return authzDeny;
|
|
327
475
|
const runtimeDeny = runtimeAuthorityForDirectWrite(input, toolName, seams, null);
|
|
328
476
|
if (runtimeDeny !== null)
|
|
329
477
|
return runtimeDeny;
|
|
@@ -371,7 +519,24 @@ function inspectMutationGates(input, toolName, seams, options) {
|
|
|
371
519
|
}
|
|
372
520
|
}
|
|
373
521
|
const allowCode = isSpawnTool(toolName) ? "spawn-ready" : "write-ready";
|
|
522
|
+
// #1193: non-implement slash verbs must not authorize implement/spawn tooling.
|
|
523
|
+
{
|
|
524
|
+
const intent = evaluateIntentCeilingFromEnv("implement", input.environ ?? process.env);
|
|
525
|
+
if (!intent.allowed) {
|
|
526
|
+
return deny(input, "intent-ceiling-deny", toolName, intent.reason);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
374
529
|
if (!isSpawnTool(toolName)) {
|
|
530
|
+
const writeTarget = hookWriteTargetPath(input.payload);
|
|
531
|
+
const relPath = writeTarget !== null ? toProjectRelativePosix(projectRoot, writeTarget) : null;
|
|
532
|
+
// Wave 1 authz (UAT lease + human-origin grant) before runtimeAuthority (#2944 / #2948 L1–L2).
|
|
533
|
+
const authzDeny = authzForMutation(input, toolName, seams, {
|
|
534
|
+
isDirectWrite: true,
|
|
535
|
+
relPath,
|
|
536
|
+
scopePath: scope.path,
|
|
537
|
+
});
|
|
538
|
+
if (authzDeny !== null)
|
|
539
|
+
return authzDeny;
|
|
375
540
|
const runtimeDeny = runtimeAuthorityForDirectWrite(input, toolName, seams, scope.path);
|
|
376
541
|
if (runtimeDeny !== null)
|
|
377
542
|
return runtimeDeny;
|
|
@@ -431,6 +596,24 @@ export function decideHook(input, seams = {}) {
|
|
|
431
596
|
}
|
|
432
597
|
}
|
|
433
598
|
if (input.event === "session.start") {
|
|
599
|
+
// #2926: root `.no-deft-directive` wins — skip host SessionStart bookkeeping.
|
|
600
|
+
const detectOptOut = seams.detectNoDeftDirective ?? detectNoDeftDirective;
|
|
601
|
+
const optOut = detectOptOut(projectRoot);
|
|
602
|
+
if (optOut.present) {
|
|
603
|
+
const message = optOut.inconsistent
|
|
604
|
+
? `${NO_DEFT_DIRECTIVE_DISABLED_MESSAGE}. ${NO_DEFT_DIRECTIVE_INCONSISTENT_MESSAGE}`
|
|
605
|
+
: NO_DEFT_DIRECTIVE_DISABLED_MESSAGE;
|
|
606
|
+
return {
|
|
607
|
+
verdict: "allow",
|
|
608
|
+
code: "session-start-disabled",
|
|
609
|
+
event: input.event,
|
|
610
|
+
host: input.host,
|
|
611
|
+
toolName: null,
|
|
612
|
+
projectRoot,
|
|
613
|
+
message,
|
|
614
|
+
scopePath: null,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
434
617
|
try {
|
|
435
618
|
const result = (seams.sessionStart ?? runSessionStartHookWrite)(projectRoot);
|
|
436
619
|
if (result.code !== 0) {
|
|
@@ -510,6 +693,15 @@ export function decideHook(input, seams = {}) {
|
|
|
510
693
|
}
|
|
511
694
|
return inspectMutationGates(input, toolName, seams, { proposedLifecycleExempt: false });
|
|
512
695
|
}
|
|
696
|
+
// Shell/Bash and classifiable MCP: enforce scopes.push / scopes.merge (#2711).
|
|
697
|
+
// Route bare push/merge MCP names (merge_pull_request, git_push, …) even when
|
|
698
|
+
// isMcpTool is false — classifyMcpTool is the gate (dispatcher-side, no tools↔policy cycle).
|
|
699
|
+
// Does not require active-scope ritual — only runtimeAuthority when enabled.
|
|
700
|
+
if (isShellTool(toolName) ||
|
|
701
|
+
isMcpTool(toolName) ||
|
|
702
|
+
classifyMcpTool(toolName, hookMcpArgsText(input.payload)) !== null) {
|
|
703
|
+
return decideShellOrMcpRuntimeAuthority(input, toolName, seams);
|
|
704
|
+
}
|
|
513
705
|
if (!isDirectWriteTool(toolName)) {
|
|
514
706
|
return {
|
|
515
707
|
verdict: "allow",
|
|
@@ -518,7 +710,7 @@ export function decideHook(input, seams = {}) {
|
|
|
518
710
|
host: input.host,
|
|
519
711
|
toolName,
|
|
520
712
|
projectRoot,
|
|
521
|
-
message: `${toolName} is outside the P0 direct-write/spawn enforcement slice.`,
|
|
713
|
+
message: `${toolName} is outside the P0 direct-write/spawn/shell enforcement slice.`,
|
|
522
714
|
scopePath: null,
|
|
523
715
|
};
|
|
524
716
|
}
|
|
@@ -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
|