@deftai/directive-content 0.109.0 → 0.110.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/.agents/skills/deft-directive-triage/SKILL.md +3 -3
- package/Taskfile.yml +8 -1
- package/commands.md +15 -8
- package/contracts/design-critique.md +73 -12
- package/contracts/path-write-fence.md +32 -6
- package/docs/consumer-issue-label-kit.md +6 -19
- package/docs/getting-started.md +1 -1
- package/docs/grok-build-subscription-setup.md +146 -0
- package/docs/hook-root-admission.md +150 -0
- package/docs/host-surface-assumptions.md +2 -0
- package/docs/host-tool-surface-audit.md +163 -0
- package/docs/scope-provenance.md +26 -0
- package/docs/test-boundary.md +3 -1
- package/package.json +1 -1
- package/packs/skills/skills-pack-0.1.json +30 -30
- package/scm/github.md +1 -1
- package/skills/deft-directive-design-critique/SKILL.md +5 -2
- package/skills/deft-directive-design-critique/references/motion-shape.md +1 -1
- package/skills/deft-directive-portfolio-priority/SKILL.md +2 -2
- package/skills/deft-directive-refinement/SKILL.md +3 -3
- package/skills/deft-directive-swarm/references/host-grok-build.md +2 -0
- package/skills/deft-directive-triage/SKILL.md +13 -115
- package/tasks/triage-classify.yml +11 -1
- package/templates/agents-entry.md +3 -3
- package/templates/design-critique-brief.md +3 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Grok Build subscription-only setup
|
|
2
|
+
|
|
3
|
+
Agent-facing playbook for a new maintainer. Paste this file (or issue [#4035](https://github.com/deftai/directive/issues/4035)) and say: follow this playbook. The human completes browser logins when the agent stops.
|
|
4
|
+
|
|
5
|
+
This is **host auth**, not Directive `session:start` and not product work in `deftai/directive`.
|
|
6
|
+
|
|
7
|
+
Legend (RFC2119): `!`=MUST, `~`=SHOULD, `⊗`=MUST NOT.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Goal
|
|
12
|
+
|
|
13
|
+
Grok Build is the parent on SuperGrok (`grok login` / grok.com). Claude Code and Codex run only through their CLIs on subscriptions. Console API keys may still exist at User scope for other tools. Grok must not use those keys.
|
|
14
|
+
|
|
15
|
+
## Target shape
|
|
16
|
+
|
|
17
|
+
Verified 2026-08-31 on win32 (Grok 4.6 parent). Org names differ per maintainer. The auth *methods* must match.
|
|
18
|
+
|
|
19
|
+
| Surface | Required auth | Must not use |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| Grok Build parent | grok.com / `auth.x.ai` OIDC session (`grok login`) | `XAI_API_KEY`, `GROK_CODE_XAI_API_KEY`, Console BYOK `[model.*]` |
|
|
22
|
+
| Claude Code CLI | `claude.ai` team subscription (`claude auth login --claudeai`) | `ANTHROPIC_API_KEY` / `apiKeySource=ANTHROPIC_API_KEY` |
|
|
23
|
+
| Codex CLI | ChatGPT (`codex login status` → ChatGPT) | `OPENAI_API_KEY` |
|
|
24
|
+
|
|
25
|
+
Grok catalog (`grok models`) is only `grok-4.6` / `grok-4.5`. Default is `grok-4.6`.
|
|
26
|
+
|
|
27
|
+
User-scope `ANTHROPIC_API_KEY` and `OPENAI_API_KEY` may remain for non-Grok tools. Leave them set.
|
|
28
|
+
|
|
29
|
+
## Hard stops
|
|
30
|
+
|
|
31
|
+
The agent MUST NOT:
|
|
32
|
+
|
|
33
|
+
- implement product code while running this playbook
|
|
34
|
+
- unset User-scope or Machine-scope `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `XAI_API_KEY`
|
|
35
|
+
- run `claude logout`, `codex logout`, or `grok logout`
|
|
36
|
+
- add or restore BYOK `[model.*]` blocks in `~/.grok/config.toml` (opus / sonnet / fable / gpt-5.6 Console ids)
|
|
37
|
+
- print secret values (print only `set=$true` / `set=$false`)
|
|
38
|
+
- invent `~/.config/deft` on Windows (#2544)
|
|
39
|
+
|
|
40
|
+
Config is read at Grok session start. After any `config.toml` edit, start a **new** Grok session. Mid-session children will not pick up `shell_environment_policy`.
|
|
41
|
+
|
|
42
|
+
Close stdin on CLI spawns: Windows `cmd /c "… <nul"`; Unix `… </dev/null`.
|
|
43
|
+
|
|
44
|
+
## Playbook
|
|
45
|
+
|
|
46
|
+
### 1. Grok Build
|
|
47
|
+
|
|
48
|
+
Install the Grok CLI. Put it on PATH.
|
|
49
|
+
|
|
50
|
+
Human (browser): `grok login` (grok.com / SpaceXAI OAuth at `auth.x.ai`).
|
|
51
|
+
|
|
52
|
+
Confirm: `grok models` prints `You are logged in with grok.com.` and lists only `grok-4.6` / `grok-4.5`.
|
|
53
|
+
|
|
54
|
+
### 2. `~/.grok/config.toml`
|
|
55
|
+
|
|
56
|
+
Path: `~/.grok/config.toml` (Windows: `%USERPROFILE%\.grok\config.toml`).
|
|
57
|
+
|
|
58
|
+
Set:
|
|
59
|
+
|
|
60
|
+
```toml
|
|
61
|
+
[models]
|
|
62
|
+
default = "grok-4.6"
|
|
63
|
+
default_reasoning_effort = "high"
|
|
64
|
+
web_search = "grok-4.6"
|
|
65
|
+
|
|
66
|
+
# Grok Build must not meter Anthropic/OpenAI/xAI Console API.
|
|
67
|
+
# Claude/Codex go through their CLIs (subscription).
|
|
68
|
+
# User/Machine env keys stay for other tools; Grok shells do not inherit them.
|
|
69
|
+
[shell_environment_policy]
|
|
70
|
+
exclude = [
|
|
71
|
+
"ANTHROPIC_API_KEY",
|
|
72
|
+
"OPENAI_API_KEY",
|
|
73
|
+
"XAI_API_KEY",
|
|
74
|
+
"GROK_CODE_XAI_API_KEY",
|
|
75
|
+
]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Remove every `[model.<id>]` table that points at Anthropic or OpenAI Console (typical leftovers: `opus-5-high-fast`, `sonnet-5`, `fable-5`, `gpt-5.6-*`). Do not add new ones.
|
|
79
|
+
|
|
80
|
+
Do not copy unrelated `[ui]` personal settings from another maintainer.
|
|
81
|
+
|
|
82
|
+
Then start a **new** Grok session before the probes.
|
|
83
|
+
|
|
84
|
+
### 3. Claude Code CLI
|
|
85
|
+
|
|
86
|
+
Install current Claude Code. On Windows a working layout is `~\.local\bin\claude.exe` plus shims (`~\.local\bin` and `%AppData%\npm` on PATH).
|
|
87
|
+
|
|
88
|
+
Human (browser): `claude auth login --claudeai`. Use the team org the operator names.
|
|
89
|
+
|
|
90
|
+
Confirm: `claude auth status` shows `loggedIn=true`, `authMethod=claude.ai`, `subscriptionType=team`, and **no** `apiKeySource=ANTHROPIC_API_KEY`.
|
|
91
|
+
|
|
92
|
+
### 4. Codex CLI
|
|
93
|
+
|
|
94
|
+
Install Codex. On Windows the hashed binary may live under `%LOCALAPPDATA%\OpenAI\Codex\bin\`; keep a `codex` shim on PATH.
|
|
95
|
+
|
|
96
|
+
Human (browser): `codex login` (ChatGPT).
|
|
97
|
+
|
|
98
|
+
Confirm: `codex login status` prints `Logged in using ChatGPT`.
|
|
99
|
+
|
|
100
|
+
### 5. PATH
|
|
101
|
+
|
|
102
|
+
User PATH includes `~/.local/bin` (Windows: `%USERPROFILE%\.local\bin`) so Grok children find `claude` and `codex` without extra env surgery.
|
|
103
|
+
|
|
104
|
+
## Verification (report pass/fail with evidence)
|
|
105
|
+
|
|
106
|
+
Run from a **Grok** `run_terminal_command` child after the new session. Close stdin. Never print key values.
|
|
107
|
+
|
|
108
|
+
1. **Catalog.** `grok models` → only `grok-4.6` / `grok-4.5`. Fail if opus / sonnet / fable / gpt-5.6 appear in *this* catalog (Codex may still say `gpt-5.6-*` as *its* ChatGPT model; that is not Grok BYOK).
|
|
109
|
+
|
|
110
|
+
2. **Env policy.** This process may still have User keys. Print only `set=$true/$false` for User / Machine / Process scope of `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY`, `GROK_CODE_XAI_API_KEY`. The Grok child Process-scope for `ANTHROPIC_API_KEY` and `OPENAI_API_KEY` MUST be `$false`. Fail if the child still has them — policy did not apply (old session, or config not loaded). Do not delete User keys.
|
|
111
|
+
|
|
112
|
+
3. **Claude from Grok, no extra unset.** `claude auth status` as above. Then:
|
|
113
|
+
|
|
114
|
+
`claude -p "Do not use tools. Reply with the single word: pong" --output-format text`
|
|
115
|
+
|
|
116
|
+
Fail on an API-key warning or not-logged-in.
|
|
117
|
+
|
|
118
|
+
4. **Codex from Grok.** `codex login status` → ChatGPT. Then:
|
|
119
|
+
|
|
120
|
+
`codex exec --ephemeral --skip-git-repo-check --sandbox read-only "Do not use tools. Reply with the single word: pong"`
|
|
121
|
+
|
|
122
|
+
Fail if it demands `OPENAI_API_KEY`.
|
|
123
|
+
|
|
124
|
+
5. **Grok itself.** The session model is `grok-4.6` (or `grok-4.5`), not a Claude id. `XAI_API_KEY` unset. Auth is grok.com session (`grok models` / `~/.grok/auth.json` `auth_mode=oidc` at `auth.x.ai`). Do not dump tokens from `auth.json`.
|
|
125
|
+
|
|
126
|
+
**Pass.** All five true. Grok Build is subscription-only: xAI login + Claude team + Codex ChatGPT. Keys may remain in User env for non-Grok tools.
|
|
127
|
+
|
|
128
|
+
**Fail.** Child shells still inherit `ANTHROPIC_API_KEY` (old session / policy not loaded) or Claude reports `apiKeySource=ANTHROPIC_API_KEY`. Do not delete User keys. Report and stop.
|
|
129
|
+
|
|
130
|
+
## Design-critique dispatch
|
|
131
|
+
|
|
132
|
+
When a Grok Build parent runs an N≥3 design-critique panel, this playbook's CLIs are the other-family launchers. Probe `claude` / `codex` on PATH and CLI-spawn those seats. Paste-ready is the fallback when a CLI is absent, not the default recovery. Normative stop: [`contracts/design-critique.md`](../contracts/design-critique.md) Envelope and ceiling (#4067).
|
|
133
|
+
|
|
134
|
+
## Out of scope
|
|
135
|
+
|
|
136
|
+
- [#4027](https://github.com/deftai/directive/issues/4027) — N≥3 design-critique lean-timing. This playbook is host auth. Do not launch a 3-panel unless the operator asks.
|
|
137
|
+
- [#2520](https://github.com/deftai/directive/issues/2520) — multi-engine least-privilege *pattern*. Related theme, different artifact.
|
|
138
|
+
- Unsetting User keys that other tools still need.
|
|
139
|
+
- Product code in `deftai/directive`.
|
|
140
|
+
|
|
141
|
+
## Related Grok docs (local, after install)
|
|
142
|
+
|
|
143
|
+
- `~/.grok/docs/user-guide/02-authentication.md` — grok.com session vs `XAI_API_KEY` fallback
|
|
144
|
+
- `~/.grok/docs/user-guide/05-configuration.md` — `config.toml` precedence
|
|
145
|
+
- `~/.grok/docs/user-guide/11-custom-models.md` — BYOK `[model.*]` (do not add these)
|
|
146
|
+
- `~/.grok/docs/user-guide/18-sandbox.md` — `[shell_environment_policy]`
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Hook root admission — which tree a gated write is judged against (#3794 / #4013)
|
|
2
|
+
|
|
3
|
+
Every PreToolUse mutation carries **two** roots, and they are not interchangeable:
|
|
4
|
+
|
|
5
|
+
- **payload root** — the project root the host hook payload names.
|
|
6
|
+
- **effective root** — the working tree the write target actually lands in, chosen by
|
|
7
|
+
`admitEffectiveHookRoot` from the target path.
|
|
8
|
+
|
|
9
|
+
Deny messages name both: `payloadRoot=<path> effectiveRoot=<path>`.
|
|
10
|
+
|
|
11
|
+
This page publishes what root admission decides, which gate reads which root, and why the
|
|
12
|
+
no-toplevel case behaves the way it does. It describes shipped behaviour; it changes none of it.
|
|
13
|
+
|
|
14
|
+
## The outcomes of root admission
|
|
15
|
+
|
|
16
|
+
| Target's nearest existing ancestor resolves to | Outcome | Effective root |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| The payload root itself | admit | payload root |
|
|
19
|
+
| Another working tree sharing `--git-common-dir` with the payload root | admit | that worktree |
|
|
20
|
+
| Another Git toplevel, while the payload root is not a Git repository | admit — no containment question exists | payload root |
|
|
21
|
+
| Another Git toplevel whose `--git-common-dir` cannot be read | refuse `unproven-identity` | — (deny) |
|
|
22
|
+
| A proven different repository | refuse `foreign-repository` | — (deny) |
|
|
23
|
+
| **No Git toplevel at all** | **admit** | **payload root** |
|
|
24
|
+
|
|
25
|
+
Set level: `admitMutationTargetSet` admits every member of one mutation (the declared ApplyPatch
|
|
26
|
+
path plus every path in the patch body), short-circuits on the first refused member, and then
|
|
27
|
+
requires **one unique** effective root across the members. Two admitted roots is a
|
|
28
|
+
`worktree-span` refusal, so occupancy and ritual cannot follow only the declared path.
|
|
29
|
+
|
|
30
|
+
The last row is the subject of #4013. It fires for any destination whose nearest existing ancestor
|
|
31
|
+
directory is not inside a Git working tree — a path under the OS temp directory, a file in the home
|
|
32
|
+
directory, a scratch directory outside every checkout.
|
|
33
|
+
|
|
34
|
+
## The no-toplevel case is deliberate
|
|
35
|
+
|
|
36
|
+
**A write target with no Git toplevel is gated against the payload root. That is intended, not a
|
|
37
|
+
gap.** #3794's acceptance text required falling back to the payload root when a target worktree
|
|
38
|
+
could not be admitted, and commit `bcd9d34e` deliberately split the two situations that fallback
|
|
39
|
+
used to hold: a target that resolves to *some other* toplevel whose identity cannot be read now
|
|
40
|
+
fails closed as `unproven-identity`, while a target with *no* toplevel keeps the payload-root
|
|
41
|
+
behaviour. "A question asked and left unanswered" is the rationale for the fail-closed branch. It
|
|
42
|
+
is not the rationale for this one — no containment question was ever posed.
|
|
43
|
+
|
|
44
|
+
Three measured reasons the fallback is not a lease bug (#4013, accepted successor lean
|
|
45
|
+
[5481139589](https://github.com/deftai/directive/issues/4013#issuecomment-5481139589)):
|
|
46
|
+
|
|
47
|
+
1. **The effective root selects more than a lease.** It is the input to occupancy, the session
|
|
48
|
+
ritual, active scope, the story write fence and assist-scratch classification. "No root, no
|
|
49
|
+
gating" would drop all of those for an out-of-tree write, so an unritualed session — or an agent
|
|
50
|
+
acting on an absolute path it read in an issue — could write a home, config or temp file with no
|
|
51
|
+
ritual and no scope basis. It would also buy nothing on active scope, which already exempts
|
|
52
|
+
outside-root writes (#2885).
|
|
53
|
+
2. **Occupancy and ritual are cross-checked, and the allow path re-stamps the lease.** The admitted
|
|
54
|
+
owner is compared against the session the verified ritual is bound to. Relaxing only the
|
|
55
|
+
foreign-lease refusal either leaves the writer blocked by that comparison, or suppresses it and
|
|
56
|
+
lets one session ride another's ceremony. The same path re-stamps `heartbeat_at` immediately
|
|
57
|
+
before an allowed write, so an unrelated out-of-tree write would renew the very lease a
|
|
58
|
+
narrowing meant to decouple.
|
|
59
|
+
3. **A nullable root has no defined behaviour for a multi-target patch.** Target-set admission
|
|
60
|
+
demands one unique root. If an outside member contributed nothing, a patch mixing a
|
|
61
|
+
linked-worktree edit with an injected absolute path could collapse to the linked root and evade
|
|
62
|
+
the `worktree-span` refusal; if absence dominated instead, one outside member would suppress the
|
|
63
|
+
gates for the in-tree edit beside it. Either way an untrusted patch path becomes an
|
|
64
|
+
authority-selection input — the containment risk #3794 closed.
|
|
65
|
+
|
|
66
|
+
The friction is real: a foreign lease holder should not block an unrelated note under the OS temp
|
|
67
|
+
directory. Relieving it costs a change to the authority contract, not a bug fix. See
|
|
68
|
+
[What a narrowing would have to define](#what-a-narrowing-would-have-to-define).
|
|
69
|
+
|
|
70
|
+
## What still runs for a no-toplevel target, gate by gate
|
|
71
|
+
|
|
72
|
+
| Gate | Root it reads | Disposition |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| Occupancy lease | effective root (= payload root here) | **Runs.** A live foreign lease on the payload root denies `occupancy-occupied`; the owner or a granted member is admitted. |
|
|
75
|
+
| Session ritual, gated tier | effective root (= payload root here) | **Runs**, including the occupancy↔ritual owner cross-check and the pre-allow re-check. |
|
|
76
|
+
| Authz / UAT grant scoping | payload root | **Payload-root authoritative**, unchanged by admission. |
|
|
77
|
+
| Authz audit trail | payload root | **Payload-root authoritative.** |
|
|
78
|
+
| Kill-switch (`.deft-directive-disable`) | payload root | **Payload-root authoritative.** A flag beside the target does not disable the gate. |
|
|
79
|
+
| `deny().projectRoot` | payload root | **Payload-root authoritative**, so a deny is reported against the tree the host named. |
|
|
80
|
+
| Mutation intent ceiling (#1193) | neither | **Runs independently** of both roots. |
|
|
81
|
+
| Read-only posture | neither | **Runs.** |
|
|
82
|
+
| Runtime authority / path write fence | project policy from the payload root; story `file_scope` and the path it matches from the effective root | **Runs** when enabled. |
|
|
83
|
+
| Active scope | effective root | **Inspected, deny skipped.** The not-ready deny is skipped by the #2885 outside-root carve-out, measured from the payload root. Spawn tools have no write target and still require scope. |
|
|
84
|
+
| Assist-scratch allowlist | effective root | **No match.** An out-of-tree target is not under `.deft-scratch/` or `temp/` relative to the effective root, so the low-ceremony path does not apply. |
|
|
85
|
+
|
|
86
|
+
Read that table as the answer to "what does the lease actually protect here". The case is not a
|
|
87
|
+
lease question with one disposition; it is a root-selection question with a disposition per gate.
|
|
88
|
+
|
|
89
|
+
## Three surfaces, three behaviours
|
|
90
|
+
|
|
91
|
+
The fallback is a **direct-write** behaviour. It does not generalise across transports.
|
|
92
|
+
|
|
93
|
+
| Surface | Example tool name | Reaches root admission? | Out-of-tree destination |
|
|
94
|
+
| --- | --- | --- | --- |
|
|
95
|
+
| Direct write, bare host name | `Write`, `Edit`, `search_replace`, `ApplyPatch` | **Yes** | Judged against the payload root — the fallback above. |
|
|
96
|
+
| Generic server-prefixed MCP | `tasks__search_replace` | **No** | Routed only through push/merge runtime classification; unrecognized there, so `shell-op-unclassifiable` (fail open). |
|
|
97
|
+
| Recognized Shell file-write | `Set-Content -Path <os-temp>/note.md …` | **No** | `isInRepoShellWritePath` rejects destinations resolving outside the project root, so the #3987 reissue path skips them (fail open). |
|
|
98
|
+
|
|
99
|
+
Catalogued MCP push/merge names (`git_push`, `merge_pull_request`, …) *are* gated — by runtime
|
|
100
|
+
authority scopes, which is a different gate and does not consult root admission either.
|
|
101
|
+
|
|
102
|
+
⊗ Do not state that the payload-root fallback fires "on the direct-write and MCP surfaces". #4013's
|
|
103
|
+
issue body said that and it is wrong for generic MCP. Three surfaces, three behaviours.
|
|
104
|
+
|
|
105
|
+
## Known limitation — relative targets have no declared base (#4023)
|
|
106
|
+
|
|
107
|
+
`existingAncestorDir` resolves the write target with `resolve(targetPath)` and no base argument, so
|
|
108
|
+
a **relative** target is canonicalized against the hook **process** working directory rather than
|
|
109
|
+
the payload root or a host-supplied cwd. The same lexical target can therefore land in different
|
|
110
|
+
rows of the first table depending only on where the hook runs. Symlink and Windows-junction targets
|
|
111
|
+
shift the class the same way: a junction into a linked worktree admits that worktree, a junction
|
|
112
|
+
into a non-repository directory falls back to the payload root.
|
|
113
|
+
|
|
114
|
+
Recorded, not fixed by #4013. Canonicalization changes admission classification, so it needs its own
|
|
115
|
+
arc: **#4023**.
|
|
116
|
+
|
|
117
|
+
## What a narrowing would have to define
|
|
118
|
+
|
|
119
|
+
A narrowing is refused *as filed*, not forever. A viable successor design would:
|
|
120
|
+
|
|
121
|
+
1. Name a target-class lattice: same admitted worktree, same-repository other worktree, foreign
|
|
122
|
+
repository, unproven identity, proven no-toplevel.
|
|
123
|
+
2. State the combining rule for every mixed target set, so `admitMutationTargetSet` still yields one
|
|
124
|
+
answer.
|
|
125
|
+
3. Keep the `foreign-repository`, `unproven-identity` and `worktree-span` refusals ahead of any
|
|
126
|
+
exemption.
|
|
127
|
+
4. State, per gate in the table above, whether the exemption changes that gate — including whether a
|
|
128
|
+
no-toplevel write may refresh any worktree lease, and whose ritual satisfies it.
|
|
129
|
+
5. Canonicalize targets first (#4023).
|
|
130
|
+
|
|
131
|
+
None of that would make #4013's filed premise — that the fallback is a defect — true.
|
|
132
|
+
|
|
133
|
+
## Scope of this guarantee
|
|
134
|
+
|
|
135
|
+
Root admission is **cooperative host-session routing**, the same posture as the occupancy lease it
|
|
136
|
+
selects: hook payloads and local session ids are forgeable by a same-user process. It bounds
|
|
137
|
+
careless cross-tree writes; it is not an authentication boundary against an adversarial agent. See
|
|
138
|
+
[`contracts/path-write-fence.md`](../contracts/path-write-fence.md) for the same limit stated for
|
|
139
|
+
the write fence.
|
|
140
|
+
|
|
141
|
+
## Code
|
|
142
|
+
|
|
143
|
+
| Concern | Where |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| Root admission, single target and target set | `packages/core/src/hooks/dispatcher.ts` — `admitEffectiveHookRoot`, `admitMutationTargetSet` |
|
|
146
|
+
| Which gate reads which root | `packages/core/src/hooks/dispatcher.ts` — `inspectMutationGates` header comment |
|
|
147
|
+
| Nearest existing ancestor, toplevel and common-dir lookups | `packages/core/src/session/git.ts` — `existingAncestorDir`, `worktreePathOrNull`, `gitCommonDir` |
|
|
148
|
+
| Shell write-dest classification and the in-repo predicate | `packages/core/src/hooks/shell-write-targets.ts` — `isInRepoShellWritePath` |
|
|
149
|
+
| Tool-name surfaces | `packages/core/src/hooks/tools.ts` — `isDirectWriteTool`, `isShellTool`, `isMcpTool` |
|
|
150
|
+
| Behaviour lock | `packages/core/src/hooks/dispatcher-effective-root.test.ts` |
|
|
@@ -116,6 +116,8 @@ Where the host supports hooks, **deny writes to directive-managed paths** withou
|
|
|
116
116
|
| **Tier-1 host hooks** | PreToolUse / equivalent intercept Write/Edit/Shell that target managed paths (AGENTS managed section, deposit, pin skill paths the host exposes) |
|
|
117
117
|
| **Path write fence** | Project + story `file_scope` evaluation — [`path-write-fence.md`](../contracts/path-write-fence.md) |
|
|
118
118
|
| **Agent hook readiness** | `deft verify:hooks-installed --scope=agent --live` — [`agent-hook-readiness.md`](../contracts/agent-hook-readiness.md) |
|
|
119
|
+
| **Host tool-surface coverage** | Which tool names each host's matcher actually selects, and which are recorded out of scope — [`host-tool-surface-audit.md`](./host-tool-surface-audit.md) (#3987) |
|
|
120
|
+
| **Root admission** | Which working tree a gated write is judged against, gate by gate, and which surfaces consult it — [`hook-root-admission.md`](./hook-root-admission.md) (#3794 / #4013) |
|
|
119
121
|
| **Policy** | `plan.policy.hostHooks.<host>` — inspect with `deft policy:show --field=hostHooks` |
|
|
120
122
|
|
|
121
123
|
! When hooks are available, treat managed-path deny as the enforcement home for constitution bytes — not agent good behavior alone.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Host tool-surface audit (#3987)
|
|
2
|
+
|
|
3
|
+
The PreToolUse write gate only runs on tool names the host's deposited matcher
|
|
4
|
+
selects. A name nobody listed is not a permissive policy — it is a gate that
|
|
5
|
+
never executes. #3987 was exactly that: Grok Build's shell tool
|
|
6
|
+
`run_terminal_command` was in no matcher, so a write the gate refused could be
|
|
7
|
+
reissued through the shell and land unobserved.
|
|
8
|
+
|
|
9
|
+
This file is the recorded answer to acceptance item 2: **for every tool name a
|
|
10
|
+
supported host emits, either the deposited matcher covers it, or there is a
|
|
11
|
+
written reason it stays out of scope.** The machine-readable form is
|
|
12
|
+
`HOST_TOOL_SURFACE_AUDIT` in `packages/core/src/hooks/tools.ts`, and
|
|
13
|
+
`deft verify:hooks-installed --scope=agent` fails closed on any silence in it.
|
|
14
|
+
|
|
15
|
+
Legend (RFC2119): `!`=MUST, `~`=SHOULD, `⊗`=MUST NOT.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Two layers that can disagree
|
|
20
|
+
|
|
21
|
+
| Layer | Form | Where |
|
|
22
|
+
|-------|------|-------|
|
|
23
|
+
| Deposited matcher | literal `a\|b\|c` alternation the host matches against | `.claude/settings.json`, `.grok/hooks/deft.json`, `.cursor/hooks.json`, `.codex/hooks.json` |
|
|
24
|
+
| Runtime classifier | lowercases and strips non-alphanumerics, then set-membership | `isDirectWriteTool` / `isShellTool` / `isSpawnTool` |
|
|
25
|
+
|
|
26
|
+
Both read the same constants, so they normally agree — but a name can be
|
|
27
|
+
present in one and useless in the other. A matcher entry the classifier does not
|
|
28
|
+
recognize invokes the hook and then falls through to `not-direct-write`; a
|
|
29
|
+
classifier entry the deposit omits is never reached at all. The coverage check
|
|
30
|
+
asserts **both** for every catalogued name, so a fix has to say which layer it
|
|
31
|
+
changed.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Grok Build — fully observed
|
|
36
|
+
|
|
37
|
+
Source: this host's published tool list, plus the 5,354-call session census
|
|
38
|
+
recorded on [#3987](https://github.com/deftai/directive/issues/3987).
|
|
39
|
+
|
|
40
|
+
| Tool | Disposition |
|
|
41
|
+
|------|-------------|
|
|
42
|
+
| `write` | covered — direct write |
|
|
43
|
+
| `search_replace` | covered — direct write |
|
|
44
|
+
| `run_terminal_command` | covered — shell (#3990) |
|
|
45
|
+
| `monitor` | covered — shell; it runs an arbitrary background shell command, so omitting it reproduces the #3987 gap one tool over |
|
|
46
|
+
| `spawn_subagent` | covered — spawn |
|
|
47
|
+
| `read_file`, `grep`, `list_dir`, `search_tool`, `web_search`, `web_fetch` | out of scope — read |
|
|
48
|
+
| `todo_write` | out of scope — session-local non-product scratch |
|
|
49
|
+
| `get_command_or_subagent_output`, `wait_commands_or_subagents` | out of scope — poll over already-dispatched work |
|
|
50
|
+
| `kill_command_or_subagent` | out of scope — process control |
|
|
51
|
+
| `scheduler_delete`, `scheduler_list` | out of scope — scheduler control and read; mutate no product path |
|
|
52
|
+
| `enter_plan_mode`, `exit_plan_mode` | out of scope — session posture |
|
|
53
|
+
| `image_gen`, `image_edit`, `image_to_video`, `reference_to_video` | out of scope — generated media lands in session scratch, never a tracked product path |
|
|
54
|
+
| `scheduler_create` | **known gap, not covered** — see below |
|
|
55
|
+
| `use_tool` | **known gap, not covered** — see below |
|
|
56
|
+
|
|
57
|
+
### `scheduler_create` — spawn-class, needs a policy decision
|
|
58
|
+
|
|
59
|
+
It dispatches a background subagent on an interval, which is spawn-class by the
|
|
60
|
+
same reading that puts `spawn_subagent` in the matcher. Covering it routes a
|
|
61
|
+
scheduling primitive through the full spawn stack (session ritual plus an active
|
|
62
|
+
xBRIEF), which denies a shape that works today — a new deny class, not a
|
|
63
|
+
coverage repair. That is a deliberate policy call and belongs in its own change,
|
|
64
|
+
not in a matcher edit.
|
|
65
|
+
|
|
66
|
+
### `use_tool` — mcp-class, needs a classifier change
|
|
67
|
+
|
|
68
|
+
`use_tool` is a proxy: the MCP tool actually invoked is nested in
|
|
69
|
+
`tool_input.tool_name`, and the dispatcher classifies on the outer name only. A
|
|
70
|
+
matcher entry alone would buy a hook invocation and no enforcement, because
|
|
71
|
+
`isMcpTool("use_tool")` is false and `classifyMcpTool` returns null. Reading the
|
|
72
|
+
inner name is a classifier change with its own untrusted-input surface.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Claude, Codex, Cursor — partly or wholly unobserved
|
|
77
|
+
|
|
78
|
+
The deposit asserting a spelling is **not** evidence that the host emits it. All
|
|
79
|
+
four deposits carry the identical shell matcher, which is a matcher-string fact,
|
|
80
|
+
not a coverage fact ([#3987 comment 5471374558](https://github.com/deftai/directive/issues/3987#issuecomment-5471374558) F8).
|
|
81
|
+
|
|
82
|
+
| Host | Established | Unobserved |
|
|
83
|
+
|------|-------------|------------|
|
|
84
|
+
| Claude Code | shell `Bash` | direct-write and spawn spellings |
|
|
85
|
+
| Codex | shell `shell`; `apply_patch` write form (#3614) | everything else |
|
|
86
|
+
| Cursor | nothing | the whole surface |
|
|
87
|
+
|
|
88
|
+
Cursor is the one that matters: nothing in this tree observes which tool names
|
|
89
|
+
it emits on `preToolUse`. The fixture corpus asserts the framework's own
|
|
90
|
+
assumption, and `agent-hooks.test.ts` asserts the deposit contains a string.
|
|
91
|
+
Neither observes the host. Cursor could be a second zero-coverage host by the
|
|
92
|
+
exact mechanism that produced #3987, and the evidence available here would not
|
|
93
|
+
distinguish that from working coverage.
|
|
94
|
+
|
|
95
|
+
! Closing these requires an **observed** PreToolUse payload per host, not a
|
|
96
|
+
re-reading of the deposit.
|
|
97
|
+
|
|
98
|
+
⊗ Fill a host's catalog from the deposit, the matcher constants, or the fixture
|
|
99
|
+
corpus. That makes the check assert its own input and hides the gap it exists
|
|
100
|
+
to find.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## The check
|
|
105
|
+
|
|
106
|
+
`deft verify:hooks-installed --scope=agent` (also run by `deft doctor`) fails
|
|
107
|
+
closed on four silences:
|
|
108
|
+
|
|
109
|
+
| Finding | Meaning |
|
|
110
|
+
|---------|---------|
|
|
111
|
+
| `missing-audit` | a supported host has no entry here at all — the "new host drops out of coverage" case |
|
|
112
|
+
| `uncovered-tool` | a catalogued mutation name is absent from every **deposited** matcher — the "renamed tool" case, read from the file rather than regenerated |
|
|
113
|
+
| `unclassified-tool` | a catalogued name the runtime classifier does not place in the same group |
|
|
114
|
+
| `unexplained-entry` | an out-of-scope entry with no reason, a host claiming full observation while naming no mutation tool, or a non-mutation entry the classifier actually gates |
|
|
115
|
+
|
|
116
|
+
A hand-edited deposit reads as **stale** first, and `deft update` is the remedy
|
|
117
|
+
for that; the coverage message is for gaps a current deposit still leaves.
|
|
118
|
+
|
|
119
|
+
Implementation: `packages/core/src/init-deposit/host-tool-coverage.ts`.
|
|
120
|
+
Catalog: `packages/core/src/hooks/tools.ts`.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Why coverage alone did not close #3987
|
|
125
|
+
|
|
126
|
+
Matcher coverage is necessary and not sufficient — all three seats of the #3987
|
|
127
|
+
panel agreed. Coverage closes the reissue bypass. It does not keep the occupancy
|
|
128
|
+
lease alive, because the lease renews on a **gated write**, and eligibility
|
|
129
|
+
keeps almost all real shell traffic off that path: five recognized write verbs,
|
|
130
|
+
no dest carrying `$` / `*` / `?`, nothing out of root or in OS temp, and any
|
|
131
|
+
compound command refused before the gate. `cd <root>; <command>` is compound,
|
|
132
|
+
and the mandated Windows body-file flow (#2646 / #2744) fails all three
|
|
133
|
+
conditions at once.
|
|
134
|
+
|
|
135
|
+
The renewal half is `restampOwnerLivenessOnHookEvent`
|
|
136
|
+
(`packages/core/src/hooks/owner-liveness.ts`): the hook already resolves the
|
|
137
|
+
owner from the host payload, so a matched tool call renews the lease even when
|
|
138
|
+
it writes nothing. It is bounded to a host-authoritative owner, keeps
|
|
139
|
+
`markWrite` false, and never touches `claimed_at`, so `OCCUPANCY_MAX_LEASE_MS`
|
|
140
|
+
still ends the lease at 12 hours. It renews the tree the mutation gates
|
|
141
|
+
authorized against — a linked worktree under `.deft-scratch/`, not the payload
|
|
142
|
+
root — so a worktree write does not keep the primary checkout's lease alive
|
|
143
|
+
while the tree actually in use expires.
|
|
144
|
+
|
|
145
|
+
! A call with **no write target** proves only the tree the host named.
|
|
146
|
+
`projectRootFromHookPayload` takes that from the payload's own `cwd`-class
|
|
147
|
+
fields, so a session working inside a worktree names the worktree and renews
|
|
148
|
+
it. When the host reports a root that is not the tree the work is in, and no
|
|
149
|
+
write target says otherwise, nothing is renewed. That is deliberate: renewing a
|
|
150
|
+
guessed tree would keep a lease alive for a tree nobody occupies, and reclaiming
|
|
151
|
+
an unused tree is what the TTL is for. `deft occupancy:heartbeat --session-id
|
|
152
|
+
<owner>` remains the explicit path for a long, quiet session.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Cross-links
|
|
157
|
+
|
|
158
|
+
| Topic | Where |
|
|
159
|
+
|-------|-------|
|
|
160
|
+
| Gate integrity (do not clear red by editing the gate) | [`gate-integrity.md`](./gate-integrity.md) |
|
|
161
|
+
| Host honesty limits | [`host-surface-assumptions.md`](./host-surface-assumptions.md) |
|
|
162
|
+
| Agent hook readiness | [`agent-hook-readiness.md`](../contracts/agent-hook-readiness.md) |
|
|
163
|
+
| Occupancy lease and its bounds | `packages/core/src/session/occupancy.ts` |
|
package/docs/scope-provenance.md
CHANGED
|
@@ -156,3 +156,29 @@ git add .deft/approved-scope/<plan-id>.json .deft/approved-scope/<plan-id>.inten
|
|
|
156
156
|
```
|
|
157
157
|
|
|
158
158
|
Editing the xBRIEF alone does not authorize new implementation paths.
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
## Cohort-created scope (operator mint at allocation) (#3874)
|
|
162
|
+
|
|
163
|
+
If a swarm cohort brief should declare `file_scope`, the operator who approved
|
|
164
|
+
the cohort mints the digest on a real TTY at allocation time and lands
|
|
165
|
+
`.deft/approved-scope/<plan-id>.json` on the merge base before workers activate.
|
|
166
|
+
|
|
167
|
+
- Allocation-context tokens (#1378) are **not** provenance.
|
|
168
|
+
- Swarm briefs are **not** exempt from this gate.
|
|
169
|
+
- `#3110` human-presence mint and merge-base layering are unchanged.
|
|
170
|
+
|
|
171
|
+
If the operator does not mint, omit `file_scope` when authoring the cohort brief.
|
|
172
|
+
Undeclared cohort briefs are then a deliberate outcome, not a post-failure
|
|
173
|
+
workaround. Do not undeclare a declared `file_scope` to pass the gate.
|
|
174
|
+
|
|
175
|
+
## Working-tree / untracked files
|
|
176
|
+
|
|
177
|
+
`verify:scope-provenance` unions:
|
|
178
|
+
|
|
179
|
+
1. `git diff --name-only <base>...HEAD`
|
|
180
|
+
2. `git diff --name-only HEAD`
|
|
181
|
+
3. `git ls-files --others --exclude-standard`
|
|
182
|
+
|
|
183
|
+
and lists on-disk `xbrief/active/` files. Presence in the working tree is what
|
|
184
|
+
matters. Committing or not committing the brief does not hide it from the gate.
|
package/docs/test-boundary.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Test / source boundary (`verify:test-boundary`)
|
|
2
2
|
|
|
3
|
-
Refs: #3145 · Related: #1310 forward-coverage, testing layout guidance
|
|
3
|
+
Refs: #3145 · Related: #1310 / #4009 forward-coverage, testing layout guidance
|
|
4
|
+
|
|
5
|
+
`verify:forward-coverage` reuses this policy for source-to-test correspondence. It does not add a second testRoots config.
|
|
4
6
|
|
|
5
7
|
## Problem
|
|
6
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deftai/directive-content",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.110.0",
|
|
4
4
|
"deftConsumerDeposit": true,
|
|
5
5
|
"description": "Shippable Directive framework content in the consumer .deft/core/ layout (C1 flatten), plus the engine surfaces (.githooks/, Taskfile.yml, tasks/) the deposit wires. Python-free per #2022 Phase 3. Refs #11, #1669, #1967.",
|
|
6
6
|
"license": "MIT",
|