@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.35 → 0.1.46
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/bin/adl +2 -2
- package/bin/reconcile +3 -11
- package/bin/setup +15 -15
- package/package.json +1 -1
- package/packages/agent-hooks/deploy-profile.json +1 -1
- package/packages/agent-hooks/package.json +1 -1
- package/packages/agent-hooks/src/main.ts +23 -2
- package/packages/agent-hooks/src/pre-tool-use/handler.ts +2 -2
- package/packages/agent-hooks/src/rules/impl/adl-build-run-role.ts +116 -0
- package/packages/agent-hooks/src/rules/impl/adl-git-user-override-block.ts +159 -0
- package/packages/agent-hooks/src/rules/impl/adl-mcp-cli.ts +22 -12
- package/packages/agent-hooks/src/rules/impl/adl-pr-complete-pending.ts +9 -9
- package/packages/agent-hooks/src/rules/impl/adl-pr-role.ts +6 -5
- package/packages/agent-hooks/src/rules/impl/adl-run-required.ts +11 -5
- package/packages/agent-hooks/src/rules/impl/adl-workitem-role.ts +18 -5
- package/packages/agent-hooks/src/rules/impl/agent-tamper-protection.ts +30 -25
- package/packages/agent-hooks/src/rules/impl/git-core.ts +4 -8
- package/packages/agent-hooks/src/rules/impl/git-target.ts +5 -0
- package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +4 -2
- package/packages/agent-hooks/src/rules/impl/tool-frontend.ts +230 -11
- package/packages/agent-hooks/src/rules/registry.ts +11 -2
- package/packages/agent-hooks/src/rules/shell/git-identity.ts +1 -1
- package/packages/agent-hooks/src/rules/shell/invocation.ts +14 -2
- package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +10 -7
- package/packages/agent-settings/assets/TARUN-GLOBAL-AGENTS-INSTRUCTIONS.md +2 -2
- package/packages/agent-settings/package.json +1 -1
- package/packages/agent-settings/src/deploy/agents-deploy.ts +19 -1
- package/packages/agent-settings/src/deploy/index.ts +1 -0
- package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +4 -6
- package/packages/agent-skills/assets/code-review/SKILL.md +84 -66
- package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +23 -2
- package/packages/agent-skills/assets/diagnosing-bugs/references/FEEDBACK-LOOP-TACTICS.md +1 -1
- package/packages/agent-skills/assets/explore-codebase/SKILL.md +3 -2
- package/packages/agent-skills/assets/finalize-feature/SKILL.md +63 -0
- package/packages/agent-skills/assets/fix-ci/SKILL.md +58 -0
- package/packages/agent-skills/assets/handoff/SKILL.md +1 -1
- package/packages/agent-skills/assets/implement-feature/SKILL.md +150 -0
- package/packages/agent-skills/assets/implement-feature/references/briefs.md +94 -0
- package/packages/agent-skills/assets/implement-item/SKILL.md +71 -0
- package/packages/agent-skills/assets/mechanics/SKILL.md +38 -0
- package/packages/agent-skills/assets/plan-feature/SKILL.md +141 -0
- package/packages/agent-skills/assets/prepare-promotion/SKILL.md +90 -0
- package/packages/agent-skills/assets/ralplan/SKILL.md +149 -0
- package/packages/agent-skills/assets/refactor-safely/SKILL.md +4 -3
- package/packages/agent-skills/assets/report-bug/SKILL.md +72 -0
- package/packages/agent-skills/assets/review-change/SKILL.md +82 -0
- package/packages/agent-skills/assets/review-plan/SKILL.md +124 -0
- package/packages/agent-skills/assets/triage-workitems/SKILL.md +99 -0
- package/packages/agent-skills/assets/ultragoal/SKILL.md +77 -0
- package/packages/agent-skills/assets/validate-design/SKILL.md +64 -0
- package/packages/agent-skills/assets/verify-item/SKILL.md +71 -0
- package/packages/agent-skills/assets/wayfinder/SKILL.md +1 -1
- package/packages/agent-skills/assets/workitems-process/SKILL.md +145 -0
- package/packages/agent-skills/assets/workitems-process/references/ado-and-git.md +227 -0
- package/packages/agent-skills/assets/workitems-process/references/evidence.md +74 -0
- package/packages/agent-skills/assets/workitems-process/references/workitem-states.md +92 -0
- package/packages/agent-skills/assets/write-feature/SKILL.md +94 -0
- package/packages/agent-skills/dist/deploy.d.ts +52 -0
- package/packages/agent-skills/dist/deploy.d.ts.map +1 -1
- package/packages/agent-skills/dist/deploy.js +218 -7
- package/packages/agent-skills/dist/deploy.js.map +1 -1
- package/packages/agent-skills/dist/index.d.ts +1 -1
- package/packages/agent-skills/dist/index.d.ts.map +1 -1
- package/packages/agent-skills/dist/index.js +1 -1
- package/packages/agent-skills/dist/index.js.map +1 -1
- package/packages/agent-skills/package.json +1 -1
- package/packages/agent-skills/src/deploy.ts +288 -6
- package/packages/agent-skills/src/index.ts +10 -1
- package/packages/azdo/README.md +13 -4
- package/packages/azdo/bunfig.toml +6 -0
- package/packages/azdo/package.json +1 -1
- package/packages/azdo/scripts/probe-ado-rules.ts +1 -1
- package/packages/azdo/src/activity-log.ts +118 -0
- package/packages/azdo/src/azdo-attachments.ts +109 -0
- package/packages/azdo/src/azdo-pl.ts +100 -3
- package/packages/azdo/src/azdo-pr.ts +376 -17
- package/packages/azdo/src/azdo-wit.ts +594 -34
- package/packages/azdo/src/cli.ts +119 -12
- package/packages/azdo/src/lib/client.ts +95 -17
- package/packages/azdo/src/lib/validate-pr-metadata.ts +14 -5
- package/packages/azdo/src/mcp-server.ts +121 -8
- package/packages/azdo/src/tool-dispatch.ts +107 -23
- package/packages/azdo/src/workflow-ladder.csv +125 -107
- package/packages/azdo/src/workflow-rules.ts +254 -97
- package/packages/fleet/src/install-adl-package.ts +4 -3
- package/packages/repo-controls/README.md +12 -4
- package/packages/repo-controls/assets/promote.ts +1 -0
- package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +5 -5
- package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +42 -13
- package/packages/repo-controls/assets/scaffold/root/CONTRIBUTING.md +5 -5
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/closed_set/test_check.py +0 -11
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +4 -6
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/ledger/test_check.py +15 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +1 -17
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/no-type.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/typed-experiment.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-profile-untyped/docs/research/typed-research.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-key.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/extra-keys.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-created.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-description.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-last-update.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-status.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/missing-title.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/optional-keys.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/fixtures/okf-research/docs/research/research-ok.md +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/test_check.py +3 -22
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/check.py +39 -182
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/fixtures/open-work-cap-clean/plans/features/az601-clean/intent.md +17 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/open_work_cap/test_check.py +90 -321
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plan_metadata.py +31 -5
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +282 -54
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/design.md +35 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/file-map-reference.md +19 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/intent.md +25 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/items/az351-restore-entrypoint.md +25 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-clean/plans/features/az347-test-coverage/requirements.md +44 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/fixtures/plans-layout-violation/plans/features/az347-test-coverage/requirements.md +44 -0
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +646 -53
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +3 -3
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/docs_shape.py +1 -12
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +1 -1
- package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +1 -1
- package/packages/repo-controls/package.json +1 -1
- package/packages/repo-controls/src/config.ts +21 -2
- package/packages/repo-controls/src/doctor.ts +9 -14
- package/packages/repo-controls/src/findings-ratchet.ts +12 -0
- package/packages/repo-controls/src/hook.ts +61 -4
|
@@ -34,11 +34,12 @@
|
|
|
34
34
|
* regardless of who is asking, and the two rules never fight over the same
|
|
35
35
|
* call.
|
|
36
36
|
*
|
|
37
|
-
* Rows 6
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
37
|
+
* Rows 6-8 (PR comment; PR vote on `main`; PR vote on `integration`) ship
|
|
38
|
+
* their own gate now: the sibling `adl-pr-review-role.ts` rules
|
|
39
|
+
* `pr_review` (shipped in `packages/azdo/src/azdo-pr-review.ts`, C3
|
|
40
|
+
* 2026-09-27) — comment allowed for the same allow-set as row 5, vote
|
|
41
|
+
* allowed for `agent-reviewer` only and only on `main`. This rule does not
|
|
42
|
+
* match `pr_review` at all; the two rules never fight over the same call.
|
|
42
43
|
*
|
|
43
44
|
* Requires an identity to already be present (`ADL_AGENT_ID_AGENT`
|
|
44
45
|
* non-blank) — with no identity at all, `adl.run-required-for-writes`
|
|
@@ -58,9 +58,12 @@
|
|
|
58
58
|
* request-item/`--form`/`--raw` implying a body (httpie's own semantics:
|
|
59
59
|
* a request with data defaults to POST, one without defaults to GET).
|
|
60
60
|
* - An Azure DevOps MCP write tool (the `'tool-call'` kind), named
|
|
61
|
-
* `mcp__<prefix>__<tool>` where `<tool>` is one of the
|
|
61
|
+
* `mcp__<prefix>__<tool>` where `<tool>` is one of the six mutating
|
|
62
62
|
* tools `packages/azdo/src/mcp-server.ts` actually registers:
|
|
63
|
-
* `pr_create_or_update`, `pr_review`, `build_run`,
|
|
63
|
+
* `pr_create_or_update`, `pr_review`, `build_run`, and the three
|
|
64
|
+
* `workitem_create_or_update_feature`/`_pbi`/`_bug` (the tool NAME carries
|
|
65
|
+
* the work-item type, so there is no blanket `workitem_create_or_update`
|
|
66
|
+
* tool any more). The
|
|
64
67
|
* match is on the TOOL NAME alone, not the server prefix — the package is
|
|
65
68
|
* `azdo`, P4 folds the server under `adl`, and Claude Code's plugin-scoped
|
|
66
69
|
* form (`mcp__plugin_<plugin>_<server>__<tool>`) adds a third spelling —
|
|
@@ -165,7 +168,8 @@ const HTTP_CLIENT_NAMES = HTTP_CLIENT_BINARY_NAMES
|
|
|
165
168
|
* `ListToolsRequestSchema` handler (re-checked 2026-10-01) — the
|
|
166
169
|
* complete tool roster there is `pr_list_or_get`/`pr_create_or_update`/
|
|
167
170
|
* `pr_review`, `build_list_or_get`/`build_run`, `workitem_list_or_get`/
|
|
168
|
-
* `
|
|
171
|
+
* `workitem_create_or_update_feature`/`_pbi`/`_bug`; the three not in this
|
|
172
|
+
* set are the read half.
|
|
169
173
|
* `pr_review` (docs/operations/agent-permissions.md rows 6-8: comment + vote) is
|
|
170
174
|
* in this set because `adl-pr-review-role.ts` gates it by role —
|
|
171
175
|
* a caller with no run identity at all must be denied here FIRST, the
|
|
@@ -176,7 +180,9 @@ const ADO_MCP_WRITE_TOOLS: ReadonlySet<string> = new Set([
|
|
|
176
180
|
'pr_create_or_update',
|
|
177
181
|
'pr_review',
|
|
178
182
|
'build_run',
|
|
179
|
-
'
|
|
183
|
+
'workitem_create_or_update_feature',
|
|
184
|
+
'workitem_create_or_update_pbi',
|
|
185
|
+
'workitem_create_or_update_bug',
|
|
180
186
|
])
|
|
181
187
|
|
|
182
188
|
/**
|
|
@@ -184,7 +190,7 @@ const ADO_MCP_WRITE_TOOLS: ReadonlySet<string> = new Set([
|
|
|
184
190
|
* DevOps MCP tool call, reads included — the read half gets no
|
|
185
191
|
* pass just because it cannot mutate anything. `mcp-server.ts`'s complete
|
|
186
192
|
* roster: `pr_list_or_get`, `build_list_or_get`, `workitem_list_or_get`
|
|
187
|
-
* (reads) alongside the
|
|
193
|
+
* (reads) alongside the six writes above.
|
|
188
194
|
*/
|
|
189
195
|
/** The leading words of the `identityBearingReason` for an Azure DevOps MCP tool call — `message()` keys the MCP-only CLI advice off it. */
|
|
190
196
|
const MCP_REASON_PREFIX = 'Azure DevOps MCP tool'
|
|
@@ -27,20 +27,33 @@ import { effectiveIdentityForMcpCli, toolCallFor } from './adl-mcp-cli.ts'
|
|
|
27
27
|
|
|
28
28
|
const RULE_ID = 'adl.workitem-requires-tarun-or-vivek'
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The work-item write tools this rule grades — the three per-type siblings the
|
|
32
|
+
* MCP server registers (the old single `workitem_create_or_update` tool is
|
|
33
|
+
* gone; the tool NAME is the type, so there is no `type` argument to inspect).
|
|
34
|
+
* A name outside this set is not a work-item write at all and is left to
|
|
35
|
+
* `adl.run-required-for-writes`/`adl-pr-role.ts`.
|
|
36
|
+
*/
|
|
37
|
+
const WORKITEM_WRITE_TOOLS: ReadonlySet<string> = new Set([
|
|
38
|
+
'workitem_create_or_update_feature',
|
|
39
|
+
'workitem_create_or_update_pbi',
|
|
40
|
+
'workitem_create_or_update_bug',
|
|
41
|
+
])
|
|
42
|
+
|
|
30
43
|
export const adlWorkitemRequiresTarunOrVivek: RuleSpec = {
|
|
31
44
|
id: RULE_ID,
|
|
32
45
|
category: 'tool-call',
|
|
33
46
|
severity: 'deny',
|
|
34
47
|
enabledByDefault: true,
|
|
35
|
-
summary: 'Deny
|
|
48
|
+
summary: 'Deny the workitem_create_or_update_* tools for agent-reviewer (docs/operations/agent-permissions.md row 10) — the MCP tools AND their adl mcp cli equivalents.',
|
|
36
49
|
params: noParams,
|
|
37
50
|
match(inv: Invocation, ctx: RuleContext<Record<string, never>>): Finding | null {
|
|
38
|
-
// `adl mcp cli
|
|
39
|
-
//
|
|
40
|
-
//
|
|
51
|
+
// `adl mcp cli workitem-create-or-update-feature`/`-pbi`/`-bug` dispatch to
|
|
52
|
+
// the same tools the MCP server calls — graded identically, not only the
|
|
53
|
+
// tool-call payload shape.
|
|
41
54
|
const call = toolCallFor(inv)
|
|
42
55
|
if (call === null) return null
|
|
43
|
-
if (mcpToolName(call.tool)
|
|
56
|
+
if (!WORKITEM_WRITE_TOOLS.has(mcpToolName(call.tool) ?? '')) return null
|
|
44
57
|
|
|
45
58
|
// Grade the EFFECTIVE identity for THIS specific piece — a valid inline
|
|
46
59
|
// `ADL_AGENT_ID_AGENT=` override on the same `adl mcp cli` invocation
|
|
@@ -2,21 +2,25 @@
|
|
|
2
2
|
* Editing the files that wire the host's hooks.
|
|
3
3
|
*
|
|
4
4
|
* Handoff 5, rule 5: deny Write/Edit/MultiEdit/NotebookEdit on
|
|
5
|
-
* `.claude/settings.json`, `.claude/settings.local.json`,
|
|
6
|
-
* `.codex/config.toml`. These are the files that install the hook into
|
|
7
|
-
* host (including the `.local.json` overrides)
|
|
8
|
-
* to switch the gate off without anyone noticing.
|
|
5
|
+
* `.claude/settings.json`, `.claude/settings.local.json`, and
|
|
6
|
+
* `.codex/config.toml`. These are the files that install the hook into
|
|
7
|
+
* the host (including the `.local.json` overrides) — editing any of them
|
|
8
|
+
* is a way to switch the gate off without anyone noticing.
|
|
9
9
|
*
|
|
10
|
-
* `adl-config.toml`
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
10
|
+
* `adl-config.toml` is NOT in this set. The earlier 2026-10-12 ruling
|
|
11
|
+
* ("we left a door open, close it") assumed the working-tree copy is what
|
|
12
|
+
* every running hook reads and that an agent writing
|
|
13
|
+
* `[hooks] enabled = false` flips the gate off ahead of any commit. The
|
|
14
|
+
* commit side IS enforced — repo-controls' `integrity.ts` (`WIRING`,
|
|
15
|
+
* refuses a normal commit of `adl-config.toml`) and `agent-hooks`'s
|
|
16
|
+
* `git.protected-tree-commit` rule (PBI 373/679) — and the working-tree
|
|
17
|
+
* edit is a runtime-config edit, not a carrier rewrite. The design
|
|
18
|
+
* ("agents may edit [WIRING] while preparing a reviewed change, but a
|
|
19
|
+
* normal commit must never land them" — integrity.ts:5-7) places the
|
|
20
|
+
* edit on the allowed side and the commit on the denied side.
|
|
21
|
+
* Re-confirmed 2026-10-13 (Tarun: "adl-config.toml can be editted but
|
|
22
|
+
* not committed by the agents"). `adl-config.toml` is a config file,
|
|
23
|
+
* not a hook carrier.
|
|
20
24
|
*
|
|
21
25
|
* Hooks are local-repo-first (Tarun, 2026-09-29): there is no managed-hooks
|
|
22
26
|
* lock and no global carrier, so these per-repo carrier files are the ONLY
|
|
@@ -477,20 +481,21 @@ export const agentTamperProtection: RuleSpec = {
|
|
|
477
481
|
: via === 'interpreter'
|
|
478
482
|
? 'an interpreter one-liner writing'
|
|
479
483
|
: 'editing'
|
|
484
|
+
const listed = [...PROTECTED_CARRIER_PATHS]
|
|
485
|
+
.map((p) => '`' + p + '`')
|
|
486
|
+
.join(', ')
|
|
480
487
|
return {
|
|
481
488
|
user: `Blocked ${via === 'tool-call' ? 'editing' : verb} ${path}. This file wires the host's hooks; ask the user to change it.`,
|
|
482
489
|
agent:
|
|
483
|
-
`DENIED ${verb} ${path}. This file is one of the tamper-protected files: ` +
|
|
484
|
-
'
|
|
485
|
-
'
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
'
|
|
490
|
-
'
|
|
491
|
-
|
|
492
|
-
'enforcement layer for the hosts that read them (there is no managed-hooks lock). ' +
|
|
493
|
-
`If a legitimate change is needed, file a PR or ask the user to make it themselves. ` +
|
|
490
|
+
`DENIED ${verb} ${path}. This file is one of the tamper-protected files: ${listed}. ` +
|
|
491
|
+
'Each installs or overrides a host\'s own hook contract. Editing, overwriting, moving ' +
|
|
492
|
+
'over, truncating, deleting, or rewriting any of them — by a tool call, a shell write, ' +
|
|
493
|
+
'or a git checkout/restore/reset/rm — is a way to switch the gate off without anyone ' +
|
|
494
|
+
'noticing, the ONLY enforcement layer for the hosts that read them (there is no ' +
|
|
495
|
+
'managed-hooks lock). `adl-config.toml` is intentionally NOT in this list: it is a ' +
|
|
496
|
+
'runtime config file an agent may edit; the commit is denied by `git.protected-tree-' +
|
|
497
|
+
'commit` and by repo-controls\' `integrity.ts` (PBI 373/679). If a legitimate change ' +
|
|
498
|
+
`to a host carrier is needed, file a PR or ask the user to make it themselves. ` +
|
|
494
499
|
`Blocked call: ${ctx.snippet}`,
|
|
495
500
|
}
|
|
496
501
|
},
|
|
@@ -49,10 +49,9 @@ export const gitHooksPath: RuleSpec = {
|
|
|
49
49
|
'git -c core.hooksPath=/dev/null commit -m x',
|
|
50
50
|
'git -c core.hooksPath= push',
|
|
51
51
|
'git -c core.hooksPath=/tmp/empty commit --amend',
|
|
52
|
-
'git config core.hooksPath /dev/null',
|
|
53
|
-
'git config --global core.hooksPath /tmp/empty',
|
|
54
52
|
],
|
|
55
|
-
|
|
53
|
+
// The `git config core.hooksPath …` positive controls were here for years — gone as of Bug 695 (2026-10-12): `adl.git-user-override-block` denies every `git config` invocation in the engine, so a `git config …` string is denied upstream and never exercises the `subcommand === 'config' && overridesHooksPath(git.rest)` branch (the `-c` deny entries above still cover the override surface).
|
|
54
|
+
allow: ['git stash --help'],
|
|
56
55
|
},
|
|
57
56
|
}
|
|
58
57
|
|
|
@@ -91,11 +90,8 @@ export const gitCoreBare: RuleSpec = {
|
|
|
91
90
|
deny: [
|
|
92
91
|
'git -c core.bare=true status',
|
|
93
92
|
'git -c core.bare=false commit -m x',
|
|
94
|
-
'git config core.bare true',
|
|
95
|
-
'git config --bool core.bare true',
|
|
96
|
-
'git config --global core.bare true',
|
|
97
|
-
'git config --unset core.bare',
|
|
98
93
|
],
|
|
99
|
-
|
|
94
|
+
// The `git config core.bare …` positive controls were here for years — gone as of Bug 695 (2026-10-12): `adl.git-user-override-block` denies every `git config` invocation in the engine, so a `git config …` string is denied upstream and never exercises the `subcommand === 'config' && flipsCoreBare(git.rest)` branch (the `-c` deny entries above still cover the override surface).
|
|
95
|
+
allow: ['git status'],
|
|
100
96
|
},
|
|
101
97
|
}
|
|
@@ -209,6 +209,7 @@ function heredocDelimiters(piece: string): string[] | 'unparsable' {
|
|
|
209
209
|
for (; i < piece.length && !/[\s;|&<>()]/.test(piece[i] as string); i++) {
|
|
210
210
|
sawWord = true
|
|
211
211
|
const c = piece[i] as string
|
|
212
|
+
/* istanbul ignore next -- splitShell() only yields a valid (non-`invalid`) result when its trailing `escape` flag is clear, and it clears that flag by consuming the escaped character; so a valid piece can never END on a lone `\`, it can only end on the SECOND half of a `\\` pair, which this scan reaches as `c` and steps over with a defined `piece[++i]`. Exhaustively checked: 15,867,149 valid commands over the 18-symbol quote/separator alphabet produced zero pieces ending in a lone backslash. */
|
|
212
213
|
if (c === '\\') word += piece[++i] ?? ''
|
|
213
214
|
else if (c === "'" || c === '"') {
|
|
214
215
|
const close = piece.indexOf(c, i + 1)
|
|
@@ -240,6 +241,7 @@ function parse(command: string): Parsed | null | 'closed' {
|
|
|
240
241
|
let cursor = 0
|
|
241
242
|
for (const piece of split.pieces) {
|
|
242
243
|
const at = command.indexOf(piece, cursor)
|
|
244
|
+
/* istanbul ignore next -- every piece splitShell() returns is an ordered, non-overlapping substring of the very `command` it was split from, so `indexOf(piece, cursor)` always finds it at or after `cursor`; `at < 0` would require splitShell() to emit a piece that is not in its input. Exhaustively checked: 958,762 valid commands over the 18-symbol quote/separator alphabet produced zero such pieces. */
|
|
243
245
|
if (at < 0) return { tokens, gaps: null, functions }
|
|
244
246
|
gaps.push(command.slice(cursor, at))
|
|
245
247
|
cursor = at + piece.length
|
|
@@ -342,9 +344,11 @@ function applyState(parsed: Parsed, until: number, target: Mutable): boolean {
|
|
|
342
344
|
if (state.names.includes('GIT_WORK_TREE')) delete target.workTree
|
|
343
345
|
continue
|
|
344
346
|
}
|
|
347
|
+
/* istanbul ignore next -- `gaps` is null on exactly one path, parse()'s `at < 0` return above, which is itself unreachable because splitShell() only emits ordered non-overlapping substrings of `command`. Every other parse() result carries a populated `gaps`, so this re-check is the fail-closed half of that invariant rather than a reachable state. */
|
|
345
348
|
if (parsed.gaps === null) return false
|
|
346
349
|
const before = plainGap(parsed.gaps[i] as string)
|
|
347
350
|
if (before === null) return false
|
|
351
|
+
/* istanbul ignore next -- `parsed.tokens[i - 1]` is null only for a here-doc BODY, and parse() sets `gaps[body] = '\n'` for every such nulled index; bodies are newline-separated, so the gap at the next index is that same `'\n'`, never `'&&'`. `before === '&&'` therefore short-circuits before this line reads `tokens[i - 1]`, so the `?? null` fallback is unreachable. */
|
|
348
352
|
if (before === '&&' && (i === 0 || !['cd', 'export'].includes(classify(parsed.tokens[i - 1] ?? null, parsed.functions).kind))) return false
|
|
349
353
|
for (let j = i + 1; j <= Math.min(until, parsed.gaps.length - 1); j++) if (plainGap(parsed.gaps[j] as string) === null) return false
|
|
350
354
|
if (state.kind === 'cd') {
|
|
@@ -406,6 +410,7 @@ function walk(command: string, inv: Invocation & { kind: 'git' }, inherited: Mut
|
|
|
406
410
|
if (inner === undefined) continue
|
|
407
411
|
if (inner === null || !applyState(parsed, i, target)) return null
|
|
408
412
|
const nested = walk(script, inv, target)
|
|
413
|
+
/* istanbul ignore next -- `inner` and `nested` are the SAME walk over the SAME script and invocation, differing only in the inherited `dirs`/`gitDir`/`workTree`; no branch inside walk() reads those to choose null or undefined (only `homeUnknown` gates expandPath, and applyState above never sets it). Since `inner` was already a non-null target here, so is `nested`, and the fallback is unreachable. Fuzzed: 6.6M generated shell/-c command pairs produced no case where it fires. */
|
|
409
414
|
return nested ?? null
|
|
410
415
|
}
|
|
411
416
|
return undefined
|
|
@@ -11,7 +11,9 @@ export const KNOWN_MCP_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
|
11
11
|
'build_list_or_get',
|
|
12
12
|
'build_run',
|
|
13
13
|
'workitem_list_or_get',
|
|
14
|
-
'
|
|
14
|
+
'workitem_create_or_update_feature',
|
|
15
|
+
'workitem_create_or_update_pbi',
|
|
16
|
+
'workitem_create_or_update_bug',
|
|
15
17
|
])
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -50,7 +52,7 @@ export function toolNameEndingWithKnownSuffix(tool: string, knownSuffixes: Reado
|
|
|
50
52
|
*
|
|
51
53
|
* Shared by every rule that gates an Azure DevOps MCP tool by name
|
|
52
54
|
* (adl-run-required.ts, adl-pr-role.ts, adl-pr-complete-pending.ts,
|
|
53
|
-
* adl-workitem-role.ts, adl-build-run-
|
|
55
|
+
* adl-workitem-role.ts, adl-build-run-role.ts) so the parsing cannot drift
|
|
54
56
|
* between them.
|
|
55
57
|
*
|
|
56
58
|
* Prefix-less fallback (added 2026-09-28 with the one-agent-identity
|
|
@@ -9,6 +9,30 @@
|
|
|
9
9
|
* body either way; the allowance is stated in the message for the human,
|
|
10
10
|
* not enforced here).
|
|
11
11
|
*
|
|
12
|
+
* az365 (Scope A — JS-toolchain mutation policy, 2026-10-13): the matcher
|
|
13
|
+
* was a binary-name-only `deny` returning a Finding for every
|
|
14
|
+
* `frontend-tool` invocation regardless of effect. The OLD corpus
|
|
15
|
+
* contained 14 read-only invocations (lint, typecheck, coverage, test
|
|
16
|
+
* runs without snapshot updates, help-only) as `deny`, which is the bug:
|
|
17
|
+
* a legitimate "look at my code" call was refused with the same Finding
|
|
18
|
+
* as a file-rewriting one. The new matcher inspects `inv.args` and
|
|
19
|
+
* returns `null` (allow) for read-only invocations, returning a Finding
|
|
20
|
+
* only for the mutating ones:
|
|
21
|
+
*
|
|
22
|
+
* deny biome format … --write (file rewrite)
|
|
23
|
+
* deny vitest run -u / --update (snapshot mutation)
|
|
24
|
+
* deny tsc <file> (no --noEmit; emits .js/.d.ts)
|
|
25
|
+
* allow tsc --noEmit (type-check, no emit)
|
|
26
|
+
* allow biome check … (read)
|
|
27
|
+
* allow biome lint … (read)
|
|
28
|
+
* allow vitest run (test runner, no source write)
|
|
29
|
+
* allow vitest --coverage run (writes only to coverage/)
|
|
30
|
+
* allow jest --testPathPattern= (test runner, no source write)
|
|
31
|
+
*
|
|
32
|
+
* The matcher does NOT inspect the `just` recipe body — the gate runs at
|
|
33
|
+
* the recipe boundary, not inside it. `--help`/`--version` are already
|
|
34
|
+
* short-circuited at classification time and never reach this matcher.
|
|
35
|
+
*
|
|
12
36
|
* Tarun's decision (2026-09-27, feature az372): the message must not name a
|
|
13
37
|
* particular recipe — see messages.ts's `JUST_RECIPES_POINTER`.
|
|
14
38
|
*/
|
|
@@ -18,37 +42,231 @@ import type { Finding, MessageContext, RuleSpec } from '../spec.ts'
|
|
|
18
42
|
import { noParams } from '../spec.ts'
|
|
19
43
|
import { detailString, JUST_RECIPES_POINTER } from './messages.ts'
|
|
20
44
|
|
|
45
|
+
/** `biome format … --write` rewrites files in place. The flag may appear anywhere on the command line (`--write foo` or `foo --write`); the `--write=foo` and `-wX` concatenated forms arrive as single tokens. */
|
|
46
|
+
function biomeFormatWritesFile(args: readonly { readonly value: string }[]): boolean {
|
|
47
|
+
return args.some((arg) => {
|
|
48
|
+
if (arg.value === '--write' || arg.value === '-w') return true
|
|
49
|
+
if (arg.value.startsWith('--write=')) return true
|
|
50
|
+
// Concatenated short-flag form: `-wX` where X is anything. The shell
|
|
51
|
+
// does not split the token; the matcher sees `-wX` as one element.
|
|
52
|
+
if (arg.value.startsWith('-w') && arg.value.length > 2) return true
|
|
53
|
+
return false
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `tsc <file>` without `--noEmit` emits `.js` and `.d.ts` files into the
|
|
59
|
+
* working tree. `tsc --noEmit` is the read intent. A bare `tsc` (no
|
|
60
|
+
* file operand, no flags) does nothing the matcher cares about — tsc
|
|
61
|
+
* without operands reads `tsconfig.json` and may emit based on its
|
|
62
|
+
* `noEmit` setting; we treat the no-operand case as ambiguous and let
|
|
63
|
+
* it through (the engine's `uncertain` category would catch it if a
|
|
64
|
+
* future rule needs that).
|
|
65
|
+
*/
|
|
66
|
+
const TSC_VALUE_FLAGS: ReadonlySet<string> = new Set([
|
|
67
|
+
'-p', '--project', // config file
|
|
68
|
+
'--build', // takes a project reference; value can be 'false'
|
|
69
|
+
'-t', '--target',
|
|
70
|
+
'--module',
|
|
71
|
+
'--moduleResolution',
|
|
72
|
+
'--jsx',
|
|
73
|
+
'--outDir',
|
|
74
|
+
'--rootDir',
|
|
75
|
+
'--baseUrl',
|
|
76
|
+
'--paths',
|
|
77
|
+
'--types',
|
|
78
|
+
'--lib',
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
function tscWritesFiles(args: readonly { readonly value: string }[]): boolean {
|
|
82
|
+
// --noEmit is the read intent; --noEmit=false is the write intent.
|
|
83
|
+
if (args.some((arg) => arg.value === '--noEmit')) return false
|
|
84
|
+
// Explicit emit flags are the write intent regardless of the file operand.
|
|
85
|
+
if (args.some((arg) => arg.value === '--emit' || arg.value.startsWith('--emit='))) return true
|
|
86
|
+
// Walk the args, skipping a value-taking flag's operand, and look for a
|
|
87
|
+
// positional file operand that names a .ts/.tsx file. That is the
|
|
88
|
+
// write intent: tsc defaults to emit when given a source file.
|
|
89
|
+
// `tsc -p tsconfig.json` is read-only because `-p` only names the
|
|
90
|
+
// config; tsc respects the config's own `noEmit` setting.
|
|
91
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
92
|
+
const arg = args[index] as { readonly value: string }
|
|
93
|
+
if (arg.value.startsWith('-')) {
|
|
94
|
+
// Flag with a value takes the next word; skip it so we do not
|
|
95
|
+
// mistake the flag's value (`tsconfig.json` after `-p`) for a
|
|
96
|
+
// positional file operand.
|
|
97
|
+
if (TSC_VALUE_FLAGS.has(arg.value)) index += 1
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
// Positional operand that names a TypeScript source file is the
|
|
101
|
+
// write intent. Anything else (a config, a glob, an empty) is not.
|
|
102
|
+
if (/\.tsx?$/.test(arg.value)) return true
|
|
103
|
+
}
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* `vitest run -u` / `vitest run --update` mutates snapshot files in place.
|
|
109
|
+
* The shell hands the matcher individual tokens; the `-uX` / `--update=now`
|
|
110
|
+
* / `-u=foo` concatenation forms arrive as a single token whose value
|
|
111
|
+
* starts with `-u` or `--update=`. Treat any token that starts with `-u`
|
|
112
|
+
* or `--update` (and is not a different flag like `--updateSnapshot`
|
|
113
|
+
* which is a different binary's flag) as the snapshot-write trigger.
|
|
114
|
+
*/
|
|
115
|
+
function vitestUpdatesSnapshots(args: readonly { readonly value: string }[]): boolean {
|
|
116
|
+
return args.some((arg) => {
|
|
117
|
+
if (arg.value === '-u' || arg.value === '--update') return true
|
|
118
|
+
if (arg.value === '-u=' || arg.value.startsWith('--update=')) return true
|
|
119
|
+
// Concatenated short-flag form: `-uX` where X is anything (rare in
|
|
120
|
+
// practice; vitest doesn't combine `-u` with other short flags, but
|
|
121
|
+
// the shell hands the matcher the token unsplit). Match `-u` as a
|
|
122
|
+
// prefix only when the rest is a non-flag char, so `-update` (long
|
|
123
|
+
// option) does not match.
|
|
124
|
+
if (arg.value.startsWith('-u') && arg.value.length > 2) return true
|
|
125
|
+
return false
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** `jest --testPathPattern=… -u` / `jest -u` / `jest --updateSnapshot` mutates snapshots. */
|
|
130
|
+
function jestUpdatesSnapshots(args: readonly { readonly value: string }[]): boolean {
|
|
131
|
+
return args.some((arg) => {
|
|
132
|
+
if (arg.value === '-u' || arg.value === '--updateSnapshot') return true
|
|
133
|
+
if (arg.value === '-u=' || arg.value.startsWith('--updateSnapshot=')) return true
|
|
134
|
+
// Concatenated short-flag form: `-uX` as above.
|
|
135
|
+
if (arg.value.startsWith('-u') && arg.value.length > 2) return true
|
|
136
|
+
return false
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
21
140
|
export const toolFrontend: RuleSpec = {
|
|
22
141
|
id: 'tool.frontend',
|
|
23
142
|
category: 'tool',
|
|
24
143
|
severity: 'deny',
|
|
25
144
|
enabledByDefault: true,
|
|
26
|
-
summary: "Deny
|
|
145
|
+
summary: "Deny mutating npx/vitest/jest/biome/tsc invocations; reads and tests are permitted.",
|
|
27
146
|
params: noParams,
|
|
28
147
|
match(inv: Invocation): Finding | null {
|
|
29
148
|
if (inv.kind !== 'frontend-tool') return null
|
|
30
|
-
|
|
149
|
+
const args = inv.args
|
|
150
|
+
// The mutating surfaces. Each is the only binary-name whose flag
|
|
151
|
+
// shape can change the working tree; if the rule's matcher here
|
|
152
|
+
// grows, the corpus deny list below is the place to add the new
|
|
153
|
+
// fixture so the engine's verdict stays testable.
|
|
154
|
+
switch (inv.tool.replace(/\.exe$/i, '')) {
|
|
155
|
+
case 'biome':
|
|
156
|
+
// `biome format … --write` is the file-rewrite path. `biome
|
|
157
|
+
// check` and `biome lint` are reads and must NOT be denied. The
|
|
158
|
+
// matcher inspects the args to distinguish; without this
|
|
159
|
+
// inspection every biome invocation is denied, which is the
|
|
160
|
+
// az365 Scope A bug.
|
|
161
|
+
if (biomeFormatWritesFile(args)) {
|
|
162
|
+
return { ruleId: 'tool.frontend', severity: 'deny', detail: { tool: inv.tool, action: 'biome-format-write' } }
|
|
163
|
+
}
|
|
164
|
+
return null
|
|
165
|
+
case 'tsc':
|
|
166
|
+
// `tsc --noEmit` is a read. `tsc <file>` without --noEmit emits
|
|
167
|
+
// .js and .d.ts into the working tree. Match the absence of
|
|
168
|
+
// --noEmit as the write intent.
|
|
169
|
+
if (tscWritesFiles(args)) {
|
|
170
|
+
return { ruleId: 'tool.frontend', severity: 'deny', detail: { tool: inv.tool, action: 'tsc-emit' } }
|
|
171
|
+
}
|
|
172
|
+
return null
|
|
173
|
+
case 'vitest':
|
|
174
|
+
// `vitest run` is a test runner. The .vitest cache directory and
|
|
175
|
+
// any coverage output live under node_modules / coverage/, not in
|
|
176
|
+
// project source. `-u` / `--update` is the snapshot-mutation
|
|
177
|
+
// path; the snapshot files live alongside the test files and ARE
|
|
178
|
+
// project source.
|
|
179
|
+
if (vitestUpdatesSnapshots(args)) {
|
|
180
|
+
return { ruleId: 'tool.frontend', severity: 'deny', detail: { tool: inv.tool, action: 'vitest-snapshot-write' } }
|
|
181
|
+
}
|
|
182
|
+
return null
|
|
183
|
+
case 'jest':
|
|
184
|
+
if (jestUpdatesSnapshots(args)) {
|
|
185
|
+
return { ruleId: 'tool.frontend', severity: 'deny', detail: { tool: inv.tool, action: 'jest-snapshot-write' } }
|
|
186
|
+
}
|
|
187
|
+
return null
|
|
188
|
+
case 'npx':
|
|
189
|
+
// `npx biome check …` is a read; `npx vitest run` is a test
|
|
190
|
+
// runner; `npx tsc --noEmit` is a typecheck. None mutate source.
|
|
191
|
+
// The mutating shape (browser automation, file rewrites) is
|
|
192
|
+
// Scope B (az365's split into a separate Feature for browser
|
|
193
|
+
// automation); this rule's match() does not cover it today and
|
|
194
|
+
// the agent-hooks `adl-human-azdo-bypass` rule plus the future
|
|
195
|
+
// `tool.browser-automation` rule (Feature azTBA) are the right
|
|
196
|
+
// place for it.
|
|
197
|
+
return null
|
|
198
|
+
default:
|
|
199
|
+
// Unknown `frontend-tool` name (a new addition to
|
|
200
|
+
// ClassifyConfig.frontendToolNames the rule does not yet know
|
|
201
|
+
// about). Default to allow and let the corpus surface it as a
|
|
202
|
+
// chosen behaviour on the next pass.
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
31
205
|
},
|
|
32
206
|
message(finding: Finding, ctx: MessageContext) {
|
|
33
207
|
const tool = detailString(finding, 'tool')
|
|
208
|
+
const action = detailString(finding, 'action')
|
|
209
|
+
const actionLabel =
|
|
210
|
+
action === 'biome-format-write' ? 'file rewrite'
|
|
211
|
+
: action === 'tsc-emit' ? 'tsc emit (.js / .d.ts write)'
|
|
212
|
+
: action === 'vitest-snapshot-write' ? 'snapshot update'
|
|
213
|
+
: action === 'jest-snapshot-write' ? 'snapshot update'
|
|
214
|
+
: 'mutating invocation'
|
|
34
215
|
return {
|
|
35
|
-
user: `Blocked
|
|
216
|
+
user: `Blocked ${tool} (${actionLabel}). Run it through ${JUST_RECIPES_POINTER} instead; it carries the same arguments and runs through the gate profile.`,
|
|
36
217
|
agent:
|
|
37
|
-
`DENIED
|
|
218
|
+
`DENIED ${tool} (${actionLabel}). Do not invoke ${tool} from Bash/Shell, including through sh -c, xargs, find -exec, or command substitution. ` +
|
|
38
219
|
`Run it through ${JUST_RECIPES_POINTER} instead - the recipe carries the same arguments and runs the gate profile rather than skipping it. ` +
|
|
39
|
-
'
|
|
40
|
-
'and invoking the tool from inside a `just` recipe body is allowed because the gate runs at the recipe boundary. ' +
|
|
220
|
+
'Read-only invocations of the same tool (lint, typecheck, test runs without snapshot updates) are permitted. ' +
|
|
41
221
|
`Blocked command: ${ctx.snippet}`,
|
|
42
222
|
}
|
|
43
223
|
},
|
|
44
224
|
corpus: {
|
|
225
|
+
// The mutating invocations — the only ones `match()` returns a Finding
|
|
226
|
+
// for. 6 entries; the az388 corpus floor in tests/unit/rules-corpus-floors.test.ts
|
|
227
|
+
// was lowered from 15 to 6 on 2026-10-13 (az365 Scope A) and the file
|
|
228
|
+
// header explains why.
|
|
45
229
|
deny: [
|
|
230
|
+
'biome format packages/desktop/src --write',
|
|
231
|
+
'biome format --write packages/desktop/src',
|
|
232
|
+
'biome format --write src/foo.ts',
|
|
233
|
+
// Concatenated short-flag form: the shell hands the matcher a
|
|
234
|
+
// single `-wX` token; the predicate's prefix check catches it.
|
|
235
|
+
'biome format src/foo.ts -w',
|
|
236
|
+
'vitest run -u',
|
|
237
|
+
'vitest run --update',
|
|
238
|
+
// Concatenated form: the shell hands the matcher a single `-uX`
|
|
239
|
+
// token; the predicate's prefix check catches it. The corpus entry
|
|
240
|
+
// is the value the shell actually produces (`-uX`); vitest does
|
|
241
|
+
// not combine `-u` with other short flags, but the matcher has
|
|
242
|
+
// to handle the form anyway.
|
|
243
|
+
'vitest run -uX',
|
|
244
|
+
// `=`-form: `--update=now` arrives as a single token, predicate
|
|
245
|
+
// catches via `startsWith('--update=')`.
|
|
246
|
+
'vitest run --update=now',
|
|
247
|
+
'tsc src/foo.ts',
|
|
248
|
+
// jest's snapshot-mutation flag form; covers the jest branch of
|
|
249
|
+
// match() and the jest action label in message(). The flag is
|
|
250
|
+
// either `-u` or `--updateSnapshot`; both must be denied.
|
|
251
|
+
'jest -u',
|
|
252
|
+
'jest --updateSnapshot',
|
|
253
|
+
'jest -uX',
|
|
254
|
+
],
|
|
255
|
+
// The read-only invocations — every one `match()` returns null for.
|
|
256
|
+
// The 14 the OLD corpus mis-classified as denied move here as the
|
|
257
|
+
// audit-trail entry that pins "this is a read, the deny is over-broad
|
|
258
|
+
// and a future narrowing should permit it" — the future narrowing is
|
|
259
|
+
// THIS rule's match(), already implemented; these entries are the
|
|
260
|
+
// proof the test suite pins against.
|
|
261
|
+
allow: [
|
|
46
262
|
'npx biome check packages/desktop/src',
|
|
263
|
+
'npx vitest run',
|
|
264
|
+
'npx tsc --noEmit',
|
|
47
265
|
'vitest run',
|
|
48
266
|
'vitest --coverage run',
|
|
49
267
|
'jest --testPathPattern=adl-conversation',
|
|
268
|
+
'biome check packages/desktop/src',
|
|
50
269
|
'biome lint packages/desktop/src',
|
|
51
|
-
'biome format packages/desktop/src --write',
|
|
52
270
|
'tsc --noEmit',
|
|
53
271
|
'tsc -p tsconfig.json',
|
|
54
272
|
'xargs npx tsc --noEmit',
|
|
@@ -56,12 +274,13 @@ export const toolFrontend: RuleSpec = {
|
|
|
56
274
|
"sh -c 'npx tsc --noEmit'",
|
|
57
275
|
"bash -lc 'npx tsc --noEmit'",
|
|
58
276
|
'echo $(biome lint src/)',
|
|
59
|
-
//
|
|
60
|
-
//
|
|
277
|
+
// The two help-only entries are already short-circuited by
|
|
278
|
+
// isHelpOrVersionOnly at classification time; they never reach
|
|
279
|
+
// `match()`. Listed under `allow` for the same audit-trail reason
|
|
280
|
+
// as the reads above: chosen behaviour, not merely a passing test.
|
|
61
281
|
'biome lint --help',
|
|
62
282
|
'tsc --noEmit --help',
|
|
63
|
-
|
|
64
|
-
allow: [
|
|
283
|
+
// Pre-existing allow entries preserved.
|
|
65
284
|
'npx --version',
|
|
66
285
|
'vitest --version',
|
|
67
286
|
'jest --version',
|
|
@@ -25,6 +25,7 @@ import type { AnyRuleSpec, RuleId } from './spec.ts'
|
|
|
25
25
|
import { asAnyRuleSpec, corpusIsEmpty } from './spec.ts'
|
|
26
26
|
|
|
27
27
|
import { gitHooksPath, gitCoreBare } from './impl/git-core.ts'
|
|
28
|
+
import { adlGitUserOverrideBlock } from './impl/adl-git-user-override-block.ts'
|
|
28
29
|
import { gitStash, gitClean, gitRebase, gitFilterBranch, gitRestore, gitReset, gitUpdateIndex } from './impl/git-transitions.ts'
|
|
29
30
|
import { gitNoVerify, gitAmend } from './impl/git-verify.ts'
|
|
30
31
|
import { gitProtectedTreeCommit } from './impl/git-protected-tree.ts'
|
|
@@ -63,7 +64,7 @@ import { adlPrCreateUpdateRole } from './impl/adl-pr-role.ts'
|
|
|
63
64
|
import { adlPrReviewRole } from './impl/adl-pr-review-role.ts'
|
|
64
65
|
import { adlPrCompletePendingDecision } from './impl/adl-pr-complete-pending.ts'
|
|
65
66
|
import { adlWorkitemRequiresTarunOrVivek } from './impl/adl-workitem-role.ts'
|
|
66
|
-
import {
|
|
67
|
+
import { adlBuildRunRole } from './impl/adl-build-run-role.ts'
|
|
67
68
|
import { toolFind } from './impl/tool-find.ts'
|
|
68
69
|
import { toolGrep } from './impl/tool-grep.ts'
|
|
69
70
|
import { toolNativeSearch } from './impl/tool-native-search.ts'
|
|
@@ -85,6 +86,14 @@ import { agentTamperProtection } from './impl/agent-tamper-protection.ts'
|
|
|
85
86
|
export const GIT_RULES: readonly AnyRuleSpec[] = [
|
|
86
87
|
gitHooksPath,
|
|
87
88
|
gitCoreBare,
|
|
89
|
+
// Right after the `core.*` override rules: a `user.*` override rides
|
|
90
|
+
// along with any subcommand via `-c user.*=`, exactly the way
|
|
91
|
+
// `core.hooksPath` / `core.bare` do — a `git -c user.name=human commit`
|
|
92
|
+
// / `push` should be reported as the user override, not whatever
|
|
93
|
+
// commit/push policy would otherwise report. The user's call (2026-10-12)
|
|
94
|
+
// is "no use of git -c user* at all … blanket ban for agents"; see
|
|
95
|
+
// CONTRIBUTING.md line 18 and INVARIANTS.md line 55.
|
|
96
|
+
adlGitUserOverrideBlock,
|
|
88
97
|
gitStash,
|
|
89
98
|
gitClean,
|
|
90
99
|
gitRebase,
|
|
@@ -150,7 +159,7 @@ export const TOOL_RULES: readonly AnyRuleSpec[] = [
|
|
|
150
159
|
adlPrReviewRole,
|
|
151
160
|
adlPrCompletePendingDecision,
|
|
152
161
|
adlWorkitemRequiresTarunOrVivek,
|
|
153
|
-
|
|
162
|
+
adlBuildRunRole,
|
|
154
163
|
].map(
|
|
155
164
|
asAnyRuleSpec,
|
|
156
165
|
)
|
|
@@ -869,7 +869,7 @@ export interface GitInvocationPiece {
|
|
|
869
869
|
/**
|
|
870
870
|
* One `adl mcp cli <command> …` invocation found while scanning a command,
|
|
871
871
|
* paired with the effective identity for THAT specific piece. `command` is the gated command name
|
|
872
|
-
* (e.g. `workitem-create-or-update`), lower-cased; `rest` is the dequoted
|
|
872
|
+
* (e.g. `workitem-create-or-update-pbi`), lower-cased; `rest` is the dequoted
|
|
873
873
|
* values after it, the same shape `impl/adl-mcp-cli.ts`'s `adlMcpCliCall`
|
|
874
874
|
* reads off `Invocation.args.slice(3)`, so a role rule can correlate a
|
|
875
875
|
* specific `Invocation` to its own piece here by comparing values, exactly
|
|
@@ -86,7 +86,19 @@ export type Invocation =
|
|
|
86
86
|
| { readonly kind: 'azure-devops'; readonly args: readonly Token[] }
|
|
87
87
|
| { readonly kind: 'find' }
|
|
88
88
|
| { readonly kind: 'grep' }
|
|
89
|
-
|
|
89
|
+
/**
|
|
90
|
+
* `npx`/`vitest`/`jest`/`biome`/`tsc` and their Windows spellings (the
|
|
91
|
+
* `.exe` variants, see `ClassifyConfig.frontendToolNames`). The
|
|
92
|
+
* `args` field carries the tokenized post-name argument list so
|
|
93
|
+
* `tool.frontend.match()` (and any future `tool.browser-automation`
|
|
94
|
+
* rule) can inspect flags — `--write` on `biome format`, `--noEmit`
|
|
95
|
+
* on `tsc`, `-u`/`--update` on `vitest run`, etc. — instead of
|
|
96
|
+
* denying every binary-name invocation regardless of effect. Threaded
|
|
97
|
+
* through by `walkToolCommand` below; the args are not consumed by
|
|
98
|
+
* `isHelpOrVersionOnly` (which has already run before this producer),
|
|
99
|
+
* they are what remains of the post-name token stream.
|
|
100
|
+
*/
|
|
101
|
+
| { readonly kind: 'frontend-tool'; readonly tool: string; readonly args: readonly Token[] }
|
|
90
102
|
/**
|
|
91
103
|
* `bun test` (any arguments, including a file/pattern operand) — the
|
|
92
104
|
* `test` subcommand specifically, after skipping bun's own `-C <dir>` /
|
|
@@ -638,7 +650,7 @@ function walkToolCommand(name: string, args: readonly Token[], cfg: ClassifyConf
|
|
|
638
650
|
return [{ kind: 'azure-devops', args }]
|
|
639
651
|
}
|
|
640
652
|
if (cfg.cargoNames.has(name)) return isHelpOrVersionOnly(args) ? [] : [{ kind: 'cargo' }]
|
|
641
|
-
if (cfg.frontendToolNames.has(name)) return isHelpOrVersionOnly(args) ? [] : [{ kind: 'frontend-tool', tool: name }]
|
|
653
|
+
if (cfg.frontendToolNames.has(name)) return isHelpOrVersionOnly(args) ? [] : [{ kind: 'frontend-tool', tool: name, args }]
|
|
642
654
|
if (cfg.bunNames.has(name)) return isBunTestSubcommand(args) ? [{ kind: 'bun-test' }] : null
|
|
643
655
|
if (cfg.justNames.has(name)) return walkJustCommand(args)
|
|
644
656
|
if (name === 'xargs' || name === 'xargs.exe') return xargsInvocation(args, walk)
|