@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
package/bin/adl
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/bin/adl
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 83039712 bytes
|
|
4
|
+
sha256: 4d5b5b238a4691c251c90bba64dbf4e528e869c23c7fac82a1f07ac330b910f6
|
package/bin/reconcile
CHANGED
|
@@ -69,8 +69,9 @@ source_mode="npm"
|
|
|
69
69
|
# can point it at a disposable test_root directory (RECONCILE_SOURCE_ONLY=1)
|
|
70
70
|
# and exercise the real sync/removal mechanics without root or a real
|
|
71
71
|
# /usr/local. The production value is the libexec dir the apps/cli
|
|
72
|
-
# commands (repo-controls,
|
|
73
|
-
#
|
|
72
|
+
# commands (repo-controls, hooks, settings) resolve the projected payload
|
|
73
|
+
# from. az697 dropped `skills` from the libexec consumers: every skill
|
|
74
|
+
# now comes from `adl skills deploy`, not from the fleet projection.
|
|
74
75
|
repo_dir_libexec_root="/usr/local/libexec/adl"
|
|
75
76
|
# Test-only escape hatch for default-agent-file.ts / managed-settings.ts's own
|
|
76
77
|
# `--root` (both already accept it; see their headers). Unset in every real
|
|
@@ -2844,9 +2845,6 @@ project_adl_package_payload() {
|
|
|
2844
2845
|
fi
|
|
2845
2846
|
done
|
|
2846
2847
|
if [[ "$mode" != apply ]]; then
|
|
2847
|
-
if [[ "$source_mode" == checkout ]]; then
|
|
2848
|
-
echo "would build agent-skills payload from checkout"
|
|
2849
|
-
fi
|
|
2850
2848
|
echo "would project Adl package payload from $repo_root to /usr/local/libexec/adl and /usr/local/bin/git{,-ado}"
|
|
2851
2849
|
if [[ "$source_mode" == npm ]]; then
|
|
2852
2850
|
echo "would link /usr/local/bin/adl to $repo_root/bin/adl"
|
|
@@ -2855,12 +2853,6 @@ project_adl_package_payload() {
|
|
|
2855
2853
|
fi
|
|
2856
2854
|
bun_bin="$(command -v bun || true)"
|
|
2857
2855
|
[[ -n "$bun_bin" ]] || { echo "ERROR Adl package projection requires bun on PATH" >&2; return 1; }
|
|
2858
|
-
if [[ "$source_mode" == checkout ]]; then
|
|
2859
|
-
(cd "$repo_root/packages/agent-skills" && "$bun_bin" run build) || {
|
|
2860
|
-
echo "ERROR could not build agent-skills payload from checkout" >&2
|
|
2861
|
-
return 1
|
|
2862
|
-
}
|
|
2863
|
-
fi
|
|
2864
2856
|
local system_root_args=()
|
|
2865
2857
|
if [[ -n "${RECONCILE_TEST_SYSTEM_ROOT:-}" ]]; then
|
|
2866
2858
|
system_root_args+=(--system-root "$RECONCILE_TEST_SYSTEM_ROOT")
|
package/bin/setup
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
-
# `sudo adl setup
|
|
4
|
+
# `sudo adl fleet setup NAME` -- the whole onboarding of a host after
|
|
5
5
|
# `bun install -g @amsterdamdatalabs/adl-agent-control` (run as the device's
|
|
6
6
|
# administrator):
|
|
7
7
|
#
|
|
@@ -45,15 +45,15 @@ while [[ "$#" -gt 0 ]]; do
|
|
|
45
45
|
*) echo "ERROR unknown argument: $1" >&2; exit 2 ;;
|
|
46
46
|
esac
|
|
47
47
|
done
|
|
48
|
-
[[ -n "$host" ]] || { echo "ERROR usage: sudo adl setup
|
|
49
|
-
[[ "$(id -u)" -eq 0 ]] || { echo "ERROR adl setup must run as root: sudo adl setup
|
|
48
|
+
[[ -n "$host" ]] || { echo "ERROR usage: sudo adl fleet setup NAME" >&2; exit 2; }
|
|
49
|
+
[[ "$(id -u)" -eq 0 ]] || { echo "ERROR adl fleet setup must run as root: sudo adl fleet setup $host" >&2; exit 1; }
|
|
50
50
|
|
|
51
51
|
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
52
52
|
package_dir="$(cd -- "$script_dir/.." && pwd)"
|
|
53
53
|
package_dir_real="$(cd -P -- "$script_dir/.." && pwd -P)"
|
|
54
54
|
# shellcheck source=lib/fleet-host.sh
|
|
55
55
|
source "$script_dir/lib/fleet-host.sh"
|
|
56
|
-
fleet_host_resolve "adl setup" "$package_dir/packages/fleet/manifest.json" "$host" || exit $?
|
|
56
|
+
fleet_host_resolve "adl fleet setup" "$package_dir/packages/fleet/manifest.json" "$host" || exit $?
|
|
57
57
|
owner="$fleet_host_owner"
|
|
58
58
|
minute="$fleet_host_minute"
|
|
59
59
|
|
|
@@ -66,23 +66,23 @@ minute="$fleet_host_minute"
|
|
|
66
66
|
source "$script_dir/lib/adl-package.sh"
|
|
67
67
|
os_name="$(uname -s)"
|
|
68
68
|
owner_home="$(adl_owner_home "$owner")" || {
|
|
69
|
-
echo "ERROR adl setup: cannot resolve the home directory of $owner" >&2
|
|
69
|
+
echo "ERROR adl fleet setup: cannot resolve the home directory of $owner" >&2
|
|
70
70
|
exit 1
|
|
71
71
|
}
|
|
72
72
|
managed_dir="$(adl_platform_package_dir "$(adl_global_node_modules "$owner_home")" "$os_name" "$(uname -m)")" || exit 1
|
|
73
73
|
managed_real="$(cd -P -- "$managed_dir" 2>/dev/null && pwd -P)" || managed_real=""
|
|
74
74
|
if [[ -z "$managed_real" || "$package_dir_real" != "$managed_real" ]]; then
|
|
75
75
|
{
|
|
76
|
-
echo "ERROR adl setup: this package ($package_dir_real) is not the package the scheduled job manages ($managed_dir)."
|
|
77
|
-
echo "ERROR adl setup: install it where the job looks, as $owner, then run setup again:"
|
|
78
|
-
echo "ERROR adl setup: bun install -g @amsterdamdatalabs/adl-agent-control"
|
|
76
|
+
echo "ERROR adl fleet setup: this package ($package_dir_real) is not the package the scheduled job manages ($managed_dir)."
|
|
77
|
+
echo "ERROR adl fleet setup: install it where the job looks, as $owner, then run setup again:"
|
|
78
|
+
echo "ERROR adl fleet setup: bun install -g @amsterdamdatalabs/adl-agent-control"
|
|
79
79
|
} >&2
|
|
80
80
|
exit 1
|
|
81
81
|
fi
|
|
82
82
|
|
|
83
83
|
adl_bin="$package_dir_real/bin/adl"
|
|
84
|
-
[[ -x "$adl_bin" ]] || { echo "ERROR adl setup: the package adl is missing or not executable: $package_dir/bin/adl" >&2; exit 1; }
|
|
85
|
-
adl_check_adl_not_shadowed "$owner" "$os_name" "$owner_home" "$package_dir_real" "adl setup" || exit 1
|
|
84
|
+
[[ -x "$adl_bin" ]] || { echo "ERROR adl fleet setup: the package adl is missing or not executable: $package_dir/bin/adl" >&2; exit 1; }
|
|
85
|
+
adl_check_adl_not_shadowed "$owner" "$os_name" "$owner_home" "$package_dir_real" "adl fleet setup" || exit 1
|
|
86
86
|
|
|
87
87
|
if [[ -n "${ADL_SETUP_TEST_ROOT:-}" ]]; then
|
|
88
88
|
system_root="${ADL_SETUP_TEST_ROOT%/}"
|
|
@@ -121,11 +121,11 @@ fi
|
|
|
121
121
|
|
|
122
122
|
mkdir -p "$system_root/var/lib/adl" "$system_root/var/log/adl"
|
|
123
123
|
|
|
124
|
-
echo "adl setup: first reconcile for $host (owner $owner)"
|
|
124
|
+
echo "adl fleet setup: first reconcile for $host (owner $owner)"
|
|
125
125
|
reconcile_rc=0
|
|
126
126
|
"$adl_bin" reconcile --host "$host" || reconcile_rc=$?
|
|
127
127
|
if [[ "$reconcile_rc" -ne 0 ]]; then
|
|
128
|
-
echo "ERROR adl setup: first reconcile failed (exit $reconcile_rc); scheduler NOT armed. Fix the failure and re-run: sudo adl setup
|
|
128
|
+
echo "ERROR adl fleet setup: first reconcile failed (exit $reconcile_rc); scheduler NOT armed. Fix the failure and re-run: sudo adl fleet setup $host" >&2
|
|
129
129
|
exit "$reconcile_rc"
|
|
130
130
|
fi
|
|
131
131
|
|
|
@@ -166,7 +166,7 @@ case "$fleet_host_platform" in
|
|
|
166
166
|
sleep 0.5
|
|
167
167
|
done
|
|
168
168
|
[[ "$unloaded" -eq 1 ]] || {
|
|
169
|
-
echo "ERROR adl setup: com.adl.global-tools is still loaded after bootout (waited 5s); not bootstrapping. Run: sudo launchctl bootout system/com.adl.global-tools" >&2
|
|
169
|
+
echo "ERROR adl fleet setup: com.adl.global-tools is still loaded after bootout (waited 5s); not bootstrapping. Run: sudo launchctl bootout system/com.adl.global-tools" >&2
|
|
170
170
|
exit 1
|
|
171
171
|
}
|
|
172
172
|
fi
|
|
@@ -214,7 +214,7 @@ esac
|
|
|
214
214
|
# would keep running stale update logic if anyone invoked it.
|
|
215
215
|
if [[ -e "$legacy_adl_sync" ]]; then
|
|
216
216
|
rm -f -- "$legacy_adl_sync"
|
|
217
|
-
echo "adl setup: removed the legacy scheduler copy $legacy_adl_sync"
|
|
217
|
+
echo "adl fleet setup: removed the legacy scheduler copy $legacy_adl_sync"
|
|
218
218
|
fi
|
|
219
219
|
|
|
220
|
-
echo "adl setup: $host reconciled and armed (every 12 hours at minute $minute); run on demand with: sudo adl reconcile --host $host"
|
|
220
|
+
echo "adl fleet setup: $host reconciled and armed (every 12 hours at minute $minute); run on demand with: sudo adl reconcile --host $host"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amsterdamdatalabs/adl-agent-control-linux-x64",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "adl CLI binary for linux/x64 (agent identity + Azure DevOps front door — see docs/architecture/cli-design.md)",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"os": [
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"file": ".opencode/plugins/adl-repo.ts",
|
|
41
41
|
"hostSuffix": "opencode",
|
|
42
42
|
"matchers": {
|
|
43
|
-
"Bash": "bash",
|
|
43
|
+
"Bash": "bash|shell",
|
|
44
44
|
"Write|Edit|MultiEdit|StrReplace|str_replace_editor|edit_file|str_replace|replace_in_file|create_file|write_file": "write|edit"
|
|
45
45
|
},
|
|
46
46
|
"unmappedMatcher": "keep",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"start": "bun src/main.ts",
|
|
9
9
|
"build": "bun ../../workflows/scripts/build/agent-hooks/build.ts",
|
|
10
10
|
"typecheck": "tsc --noEmit",
|
|
11
|
-
"test": "bun test --coverage --timeout 30000",
|
|
11
|
+
"test": "bun test --isolate --coverage --timeout 30000",
|
|
12
12
|
"check": "bun run typecheck && bun test --coverage"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
* silent no-op (exit 0, empty stdout/stderr).
|
|
27
27
|
* - `adl-config.toml` present but fails to parse/decode: fail
|
|
28
28
|
* loud (non-zero exit, reason on stderr) — never silently ignored.
|
|
29
|
+
* `pre-tool-use` exits at FAIL_CLOSED_EXIT_CODE (2); every other
|
|
30
|
+
* kind exits at the non-blocking NON_BLOCKING_FAIL_EXIT_CODE (1),
|
|
31
|
+
* matching the host-resolution and invalid-JSON-on-stdin branches
|
|
32
|
+
* above and avoiding the Claude Code/Codex `UserPromptSubmit`
|
|
33
|
+
* erase-on-exit-2 behaviour (this file's header, work item D).
|
|
29
34
|
* - `[hooks] enabled = false`: silent no-op, same as unmanaged.
|
|
30
35
|
*
|
|
31
36
|
* Every other hook kind is dispatched through the shared `HookHandler` shape
|
|
@@ -202,13 +207,29 @@ export async function run(argv: readonly string[], adlVersion?: string): Promise
|
|
|
202
207
|
|
|
203
208
|
let config: AdlRepoConfig
|
|
204
209
|
{
|
|
205
|
-
|
|
210
|
+
let text: string
|
|
211
|
+
try {
|
|
212
|
+
text = readFileSync(managed.configPath, 'utf8')
|
|
213
|
+
} catch (error) {
|
|
214
|
+
// The readFileSync race: `resolveManagedRepo` confirmed the marker
|
|
215
|
+
// exists a moment ago, but the file was unlinked, perm-removed, or
|
|
216
|
+
// became unreadable before we opened it. Same kind-conditional
|
|
217
|
+
// exit-code split as the JSON-stdin branch above and az385's
|
|
218
|
+
// host-resolution branch: pre-tool-use fails CLOSED (refuse the
|
|
219
|
+
// dangerous action), every other kind fails at the non-blocking
|
|
220
|
+
// exit 1 so a Claude Code `UserPromptSubmit` or an advisory
|
|
221
|
+
// `briefing` is never erased.
|
|
222
|
+
process.stderr.write(
|
|
223
|
+
`adl hook: cannot read ${managed.configPath}: ${String(error)}\n`,
|
|
224
|
+
)
|
|
225
|
+
return kind === 'pre-tool-use' ? FAIL_CLOSED_EXIT_CODE : NON_BLOCKING_FAIL_EXIT_CODE
|
|
226
|
+
}
|
|
206
227
|
const decoded = decodeConfig(text)
|
|
207
228
|
if (!decoded.ok) {
|
|
208
229
|
process.stderr.write(
|
|
209
230
|
`adl hook: ${describeConfigIssue(managed.configPath, decoded.issue)}\n`,
|
|
210
231
|
)
|
|
211
|
-
return
|
|
232
|
+
return kind === 'pre-tool-use' ? FAIL_CLOSED_EXIT_CODE : NON_BLOCKING_FAIL_EXIT_CODE
|
|
212
233
|
}
|
|
213
234
|
config = decoded.value
|
|
214
235
|
}
|
|
@@ -284,7 +284,7 @@ const IDENTITY_RULE_IDS: ReadonlySet<RuleId> = new Set([
|
|
|
284
284
|
* The four role-gate rules that deny a SPECIFIC Azure DevOps MCP tool call by
|
|
285
285
|
* the caller's identity (`adl.workitem-requires-tarun-or-vivek`,
|
|
286
286
|
* `adl.pr-create-update-role`, `adl.pr-review-role`,
|
|
287
|
-
* `adl.build-run-
|
|
287
|
+
* `adl.build-run-role`). None of these are in `IDENTITY_RULE_IDS` above —
|
|
288
288
|
* that set covers the rules that fire when no identity is resolved or
|
|
289
289
|
* carried, never a rule that denies WITH an identity already resolved, on the
|
|
290
290
|
* strength of who that identity is. Each of them can only fire on the same
|
|
@@ -297,7 +297,7 @@ const ROLE_GATE_RULE_IDS: ReadonlySet<RuleId> = new Set([
|
|
|
297
297
|
'adl.workitem-requires-tarun-or-vivek',
|
|
298
298
|
'adl.pr-create-update-role',
|
|
299
299
|
'adl.pr-review-role',
|
|
300
|
-
'adl.build-run-
|
|
300
|
+
'adl.build-run-role',
|
|
301
301
|
])
|
|
302
302
|
|
|
303
303
|
type IdentitySourceLabel = 'inline' | 'env' | 'default-file' | 'none'
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** `adl.build-run-role` — docs/operations/agent-permissions.md row 12 (build run), decision H1; the allow condition and the caller-declared id-is-its-own-PR-validation-build approximation (hook has no network; azdo core must re-verify) are documented on match() below and in the summary. */
|
|
2
|
+
|
|
3
|
+
import type { Invocation } from '../shell/invocation.ts'
|
|
4
|
+
import type { Finding, MessageContext, RuleContext, RuleSpec } from '../spec.ts'
|
|
5
|
+
import { noParams } from '../spec.ts'
|
|
6
|
+
import { BRANCH_PREFIXES } from '../../../../azdo/src/lib/commit-types.ts'
|
|
7
|
+
import { BUILDER_AGENTS, resolveFromEnvOrFile } from './adl-identity.ts'
|
|
8
|
+
import { detailString } from './messages.ts'
|
|
9
|
+
import { mcpToolName } from './mcp-tool.ts'
|
|
10
|
+
import { effectiveIdentityForMcpCli, toolCallFor } from './adl-mcp-cli.ts'
|
|
11
|
+
|
|
12
|
+
const RULE_ID = 'adl.build-run-role'
|
|
13
|
+
|
|
14
|
+
const PROTECTED_BRANCHES: ReadonlySet<string> = new Set(['integration', 'main'])
|
|
15
|
+
const FEATURE_PREFIXES: readonly string[] = BRANCH_PREFIXES.map((prefix) => `${prefix}/`)
|
|
16
|
+
|
|
17
|
+
type Violation = 'not-a-builder' | 'missing-id' | 'missing-branch' | 'non-feature-branch'
|
|
18
|
+
|
|
19
|
+
function normalizeBranchRef(ref: string): string {
|
|
20
|
+
return ref.startsWith('refs/heads/') ? ref.slice('refs/heads/'.length) : ref
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isFeatureBranchName(branch: string): boolean {
|
|
24
|
+
return FEATURE_PREFIXES.some((prefix) => branch.startsWith(prefix))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const adlBuildRunRole: RuleSpec = {
|
|
28
|
+
id: RULE_ID,
|
|
29
|
+
category: 'tool-call',
|
|
30
|
+
severity: 'deny',
|
|
31
|
+
enabledByDefault: true,
|
|
32
|
+
summary:
|
|
33
|
+
'Deny build_run except the lead (agent-tarun/agent-vivek) re-queuing its own PR’s validation pipeline on that PR’s feature branch (docs/operations/agent-permissions.md row 12, H1) — the MCP tool AND its adl mcp cli equivalent.',
|
|
34
|
+
params: noParams,
|
|
35
|
+
match(inv: Invocation, ctx: RuleContext<Record<string, never>>): Finding | null {
|
|
36
|
+
// Also catch `adl mcp cli build-run` — same tool, same grade.
|
|
37
|
+
const call = toolCallFor(inv)
|
|
38
|
+
if (call === null) return null
|
|
39
|
+
if (mcpToolName(call.tool) !== 'build_run') return null
|
|
40
|
+
|
|
41
|
+
// Grade the EFFECTIVE identity for THIS call — an inline ADL_AGENT_ID_AGENT= override on the same CLI invocation wins, like the other role rules.
|
|
42
|
+
const resolution = resolveFromEnvOrFile(ctx.env)
|
|
43
|
+
const fallback = resolution.ok ? resolution.agent : null
|
|
44
|
+
const agent = effectiveIdentityForMcpCli(inv, ctx.command ?? '', fallback)
|
|
45
|
+
if (agent === null) return null // no identity at all: adl.run-required-for-writes's job
|
|
46
|
+
|
|
47
|
+
// 1. Only the lead re-queues CI — the role mismatch reports first regardless of shape.
|
|
48
|
+
if (!BUILDER_AGENTS.has(agent)) {
|
|
49
|
+
return { ruleId: RULE_ID, severity: 'deny', detail: { tool: call.tool, violation: 'not-a-builder' satisfies Violation } }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. The definition id, validated exactly like azdo-pl.ts's buildRun (a positive integer).
|
|
53
|
+
const id = call.input.id
|
|
54
|
+
if (typeof id !== 'number' || !Number.isInteger(id) || id < 1) {
|
|
55
|
+
return { ruleId: RULE_ID, severity: 'deny', detail: { tool: call.tool, violation: 'missing-id' satisfies Violation } }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. The PR source branch, caller-declared exactly like pr_review's targetBranch: required, and provably a feature branch.
|
|
59
|
+
const branch = call.input.branch
|
|
60
|
+
if (typeof branch !== 'string' || branch.trim().length === 0) {
|
|
61
|
+
return { ruleId: RULE_ID, severity: 'deny', detail: { tool: call.tool, violation: 'missing-branch' satisfies Violation } }
|
|
62
|
+
}
|
|
63
|
+
const normalized = normalizeBranchRef(branch.trim())
|
|
64
|
+
if (PROTECTED_BRANCHES.has(normalized) || !isFeatureBranchName(normalized)) {
|
|
65
|
+
return { ruleId: RULE_ID, severity: 'deny', detail: { tool: call.tool, violation: 'non-feature-branch' satisfies Violation } }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null
|
|
69
|
+
},
|
|
70
|
+
message(finding: Finding, ctx: MessageContext) {
|
|
71
|
+
const tool = detailString(finding, 'tool')
|
|
72
|
+
const violation = detailString(finding, 'violation') as Violation
|
|
73
|
+
|
|
74
|
+
if (violation === 'not-a-builder') {
|
|
75
|
+
const user = 'this operation can only be done with agent-tarun or agent-vivek'
|
|
76
|
+
return {
|
|
77
|
+
user,
|
|
78
|
+
agent:
|
|
79
|
+
`DENIED ${tool}: ${user}. Only the lead may re-queue its own PR’s validation pipeline on that PR’s ` +
|
|
80
|
+
`source branch — agent-reviewer runs no implementation PRs, so it has no PR CI to re-queue. Blocked: ${ctx.snippet}`,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (violation === 'missing-id') {
|
|
85
|
+
const user = 'a build definition id is required'
|
|
86
|
+
return {
|
|
87
|
+
user,
|
|
88
|
+
agent: `DENIED ${tool}: ${user} — pass the definition id of your PR’s validation pipeline (a positive integer). Blocked: ${ctx.snippet}`,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (violation === 'missing-branch') {
|
|
93
|
+
const user = 'name the source branch of your own PR to re-queue its validation pipeline'
|
|
94
|
+
return {
|
|
95
|
+
user,
|
|
96
|
+
agent:
|
|
97
|
+
`DENIED ${tool}: ${user}. The branch argument is what lets this hook scope the re-queue to your own ` +
|
|
98
|
+
`PR’s CI — without it, pipelines run on their own triggers. Re-queue with \`branch\` naming your PR’s ` +
|
|
99
|
+
`feature branch. Blocked: ${ctx.snippet}`,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const user = 'only a feature branch that is your own PR’s source branch may be re-queued'
|
|
104
|
+
return {
|
|
105
|
+
user,
|
|
106
|
+
agent:
|
|
107
|
+
`DENIED ${tool}: ${user}. \`integration\`/\`main\` and every unrecognized branch name are denied — a PR’s ` +
|
|
108
|
+
`source branch always carries a \`${FEATURE_PREFIXES.join(' ')}\` prefix. Blocked: ${ctx.snippet}`,
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
corpus: {
|
|
112
|
+
// Tool-call rule; real coverage is tests/unit/rules-adl-build-run-role.test.ts (the shared shell corpus only proves non-firing on unrelated commands).
|
|
113
|
+
deny: [],
|
|
114
|
+
allow: ['ls -la', 'git status'],
|
|
115
|
+
},
|
|
116
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** `adl.git-user-override-block` — blanket deny (2026-10-12) of every `user.name` / `user.email` / `user.useConfigOnly` override on a git command: inline `-c user.*=…` / `--config-env user.*=…` (separated, bundled, or bare next-token), the `GIT_CONFIG_COUNT` / `GIT_CONFIG_KEY_<n>` / `GIT_CONFIG_VALUE_<n>` env-var equivalent, AND any `git config` invocation at all (read or write — the whole subcommand is closed; an agent that needs the current identity runs `git var GIT_AUTHOR_IDENT` or reads `.git/config` directly). The only allowed identity path is `ADL_AGENT_ID_AGENT=agent-tarun` on the command line so the front controller (`bin/git-ado`) stamps the right committer. Placed 3rd in `GIT_RULES`, right after the two `core.*` override rules, so a `user.*` ride-along on any subcommand reports as the user override, not the subcommand rule. The pre-commit hook (the `prepare-commit-msg` / `commit-msg` pair `bin/git-ado` calls) used to catch the *result* on `git commit`, but not on `git commit-tree` / `git fast-import` / etc. — this rule closes the gap at the command level, before git ever sees the override. */
|
|
2
|
+
|
|
3
|
+
import { isHelpOnlyInvocation } from './git-helpers.ts'
|
|
4
|
+
import type { Finding, MessageContext, RuleContext, RuleSpec } from '../spec.ts'
|
|
5
|
+
import { noParams } from '../spec.ts'
|
|
6
|
+
import { gitAgentTail } from './messages.ts'
|
|
7
|
+
import type { Invocation } from '../shell/invocation.ts'
|
|
8
|
+
import { tokenValue } from '../shell/token.ts'
|
|
9
|
+
|
|
10
|
+
/** `user.name`, `user.email`, `user.useConfigOnly` — case-insensitive. */
|
|
11
|
+
const USER_KEY = /^user\.(name|email|useconfigonly)\b/i
|
|
12
|
+
|
|
13
|
+
/** `GIT_CONFIG_COUNT=<n>` where `<n>` is a non-zero integer — captured so the env-var form's scope gate (the COUNT must be non-zero for git to read any of the KEY/VALUE pairs) is enforced, not assumed. */
|
|
14
|
+
const GIT_CONFIG_COUNT_RE = /\bGIT_CONFIG_COUNT\s*=\s*([1-9]\d*)\b/i
|
|
15
|
+
/** `GIT_CONFIG_KEY_<n>=<value>` — `<value>` is checked against `USER_KEY` separately. */
|
|
16
|
+
const GIT_CONFIG_KEY_RE = /\bGIT_CONFIG_KEY_\d+\s*=\s*([^\s'"`;&|<>(){}]+)/gi
|
|
17
|
+
|
|
18
|
+
function gitInvocation(inv: Invocation): Invocation & { kind: 'git' } | null {
|
|
19
|
+
return inv.kind === 'git' ? inv : null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** True when `tokens` (the post-`-c` value tokens) contains a `user.*` key; strips a single leading `-c` so the bundled `-cuser.name=Foo` form (whole-token consumed by `scanGitGlobals`) matches the same regex as the separated form. */
|
|
23
|
+
function tokensOverrideUser(tokens: readonly { readonly value: string }[]): boolean {
|
|
24
|
+
for (const token of tokens) {
|
|
25
|
+
const raw = tokenValue(token as Parameters<typeof tokenValue>[0])
|
|
26
|
+
// Bundled `-cuser.name=Foo`: drop the leading `-c` so the regex below matches.
|
|
27
|
+
const value = raw.startsWith('-c') && raw.length > 2 ? raw.slice(2) : raw
|
|
28
|
+
if (USER_KEY.test(value)) return true
|
|
29
|
+
}
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** True when `command` carries the `GIT_CONFIG_*` env-var form of a `user.*` override: a non-zero `GIT_CONFIG_COUNT=<n>` AND at least one `GIT_CONFIG_KEY_<n>=user.<…>` (both must be present; a lone KEY without a non-zero COUNT is a no-op and not a bypass). */
|
|
34
|
+
function envVarOverrideUser(command: string): boolean {
|
|
35
|
+
if (!GIT_CONFIG_COUNT_RE.test(command)) return false
|
|
36
|
+
// Reset the shared `g`-flag regex's lastIndex between calls (stateful).
|
|
37
|
+
GIT_CONFIG_KEY_RE.lastIndex = 0
|
|
38
|
+
for (;;) {
|
|
39
|
+
const match = GIT_CONFIG_KEY_RE.exec(command)
|
|
40
|
+
if (match === null) return false
|
|
41
|
+
if (USER_KEY.test(match[1] as string)) return true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const adlGitUserOverrideBlock: RuleSpec = {
|
|
46
|
+
id: 'adl.git-user-override-block',
|
|
47
|
+
category: 'git',
|
|
48
|
+
severity: 'deny',
|
|
49
|
+
enabledByDefault: true,
|
|
50
|
+
summary: 'Blanket deny (2026-10-12) of every `user.*` override on a git command: inline `-c` / `--config-env`, the `GIT_CONFIG_*` env-var equivalent, AND any `git config` invocation at all (read or write — the whole subcommand is closed; `git var GIT_AUTHOR_IDENT` is the only way an agent reads the current identity). The only allowed identity path is `ADL_AGENT_ID_AGENT=agent-tarun` on the command line.',
|
|
51
|
+
params: noParams,
|
|
52
|
+
match(inv, ctx: RuleContext<Record<string, never>>): Finding | null {
|
|
53
|
+
const git = gitInvocation(inv)
|
|
54
|
+
if (!git) return null
|
|
55
|
+
if (isHelpOnlyInvocation(git.rest)) return null
|
|
56
|
+
// Blanket inline-override surface: `-c user.*=…` / `--config-env user.*=…` (and the bare-token / bundled forms) land in `git.globals`.
|
|
57
|
+
if (tokensOverrideUser(git.globals)) {
|
|
58
|
+
return { ruleId: 'adl.git-user-override-block', severity: 'deny', detail: {} }
|
|
59
|
+
}
|
|
60
|
+
// Env-var form: `GIT_CONFIG_*` assignments are consumed by `walk()` before the `git` Invocation is built; scan the raw `ctx.command`.
|
|
61
|
+
if (ctx.command !== undefined && envVarOverrideUser(ctx.command)) {
|
|
62
|
+
return { ruleId: 'adl.git-user-override-block', severity: 'deny', detail: {} }
|
|
63
|
+
}
|
|
64
|
+
// Per the 2026-10-12 widening: `git config` is BANNED for agents — every invocation, read or write. An agent that needs to know the current `user.name` runs `git var GIT_AUTHOR_IDENT` or reads `.git/config` directly; `git config` of any kind is the surface that touches persisted config and is closed.
|
|
65
|
+
if (git.subcommand === 'config') {
|
|
66
|
+
return { ruleId: 'adl.git-user-override-block', severity: 'deny', detail: {} }
|
|
67
|
+
}
|
|
68
|
+
return null
|
|
69
|
+
},
|
|
70
|
+
message(_finding: Finding, ctx: MessageContext) {
|
|
71
|
+
return {
|
|
72
|
+
user:
|
|
73
|
+
'Blocked a user.name / user.email / user.useConfigOnly override on a git command. ' +
|
|
74
|
+
'Inline `-c user.*=…`, `--config-env user.*=`, `GIT_CONFIG_KEY_<n>=user.*` env-var overrides, AND any `git config` invocation are all banned in an agent session.',
|
|
75
|
+
agent:
|
|
76
|
+
'DENIED user-identity override on a git command. `git -c user.name=…` / `-c user.email=…` / ' +
|
|
77
|
+
'`--config-env user.*=…` (in every form — separated, bundled as `-cuser.*=…`, or bare ' +
|
|
78
|
+
'`-c user.* …`); the `GIT_CONFIG_COUNT=<n> GIT_CONFIG_KEY_<n>=user.* GIT_CONFIG_VALUE_<n>=…` ' +
|
|
79
|
+
'env-var form (per `git-config(1)`); AND `git config …` of any kind (read or write — the whole ' +
|
|
80
|
+
'subcommand is closed in an agent session) are blanket-denied in an agent session (2026-10-12). ' +
|
|
81
|
+
'The only allowed identity path is `ADL_AGENT_ID_AGENT=agent-tarun` on the command line: ' +
|
|
82
|
+
'`ADL_AGENT_ID_AGENT=agent-tarun git commit -m …` so the front controller (`bin/git-ado`) ' +
|
|
83
|
+
'stamps the right committer. To read the current identity, run `git var GIT_AUTHOR_IDENT` or ' +
|
|
84
|
+
'read `.git/config` directly — `git config` is NEVER the right answer in an agent session, ' +
|
|
85
|
+
`even as a one-shot. ${gitAgentTail(ctx.snippet)}`,
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
corpus: {
|
|
89
|
+
deny: [
|
|
90
|
+
// `-c user.*=` (separate token)
|
|
91
|
+
'git -c user.name=Foo commit -m x',
|
|
92
|
+
'git -c user.email=foo@bar commit -m x',
|
|
93
|
+
'git -c user.useConfigOnly=true commit -m x',
|
|
94
|
+
// `-c` bundled with `=` (no space) — the same form `scanGitGlobals` folds
|
|
95
|
+
'git -cuser.name=Foo commit -m x',
|
|
96
|
+
'git -cuser.email=foo@bar commit -m x',
|
|
97
|
+
// bare `-c` + separate-token value (next-token form)
|
|
98
|
+
'git -c user.name Foo commit -m x',
|
|
99
|
+
'git -c user.email foo@bar commit -m x',
|
|
100
|
+
// Case-insensitive
|
|
101
|
+
'git -c User.Name=Foo commit -m x',
|
|
102
|
+
'git -c USER.EMAIL=foo@bar commit -m x',
|
|
103
|
+
// `--config-env` (the env-var form, not just `-c`)
|
|
104
|
+
'git --config-env user.name=Foo commit -m x',
|
|
105
|
+
// `user.*` ride-along with a non-commit subcommand — still blocked (blanket)
|
|
106
|
+
'git -c user.name=Foo push',
|
|
107
|
+
'git -c user.name=Foo push origin main',
|
|
108
|
+
'git -c user.email=foo@bar fetch',
|
|
109
|
+
'git -c user.name=Foo log --oneline',
|
|
110
|
+
'git -c user.email=foo@bar status',
|
|
111
|
+
// The GIT_CONFIG_* env-var form — git's own alternative to `-c`; COUNT must be non-zero AND a KEY_<n>=user.* must be present for the bypass to take effect (the rule denies on that pair).
|
|
112
|
+
'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.name GIT_CONFIG_VALUE_0=Foo git commit -m x',
|
|
113
|
+
'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.email GIT_CONFIG_VALUE_0=foo@bar git commit -m x',
|
|
114
|
+
'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.useConfigOnly GIT_CONFIG_VALUE_0=true git commit -m x',
|
|
115
|
+
// Same, riding along with a non-commit subcommand
|
|
116
|
+
'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.name GIT_CONFIG_VALUE_0=Foo git push',
|
|
117
|
+
'GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=user.name GIT_CONFIG_VALUE_0=Foo GIT_CONFIG_KEY_1=user.email GIT_CONFIG_VALUE_1=foo@bar git commit -m x',
|
|
118
|
+
// Persistent form: `git config user.* …` (including `--global` / `--local` / `--unset` / `--remove-section user`).
|
|
119
|
+
'git config user.name "agent-tarun"',
|
|
120
|
+
'git config --global user.name "agent-tarun"',
|
|
121
|
+
'git config --local user.email "agent-tarun@<org-domain>"',
|
|
122
|
+
'git config user.useConfigOnly true',
|
|
123
|
+
'git config --unset user.name',
|
|
124
|
+
'git config --remove-section user',
|
|
125
|
+
'git config --remove-section-all user',
|
|
126
|
+
// Per the 2026-10-12 widening: every `git config` invocation is denied (read or write — the whole subcommand is closed; an agent that needs the current identity runs `git var GIT_AUTHOR_IDENT` or reads `.git/config` directly).
|
|
127
|
+
'git config --list',
|
|
128
|
+
'git config --list --local',
|
|
129
|
+
'git config --get user.name',
|
|
130
|
+
'git config --get-all user.email',
|
|
131
|
+
'git config --get-regexp "^user\\."',
|
|
132
|
+
'git config http.proxy http://proxy:8080',
|
|
133
|
+
'git config --global core.editor vim',
|
|
134
|
+
'git config --local init.defaultBranch main',
|
|
135
|
+
],
|
|
136
|
+
allow: [
|
|
137
|
+
// ordinary git traffic, no user.* anywhere
|
|
138
|
+
'git status',
|
|
139
|
+
'git log --oneline',
|
|
140
|
+
'git commit -m "ordinary commit"',
|
|
141
|
+
// help invocations are exempted (the only thing in `rest` is a flag)
|
|
142
|
+
'git -c user.name=Foo --help',
|
|
143
|
+
// other `-c` keys ride along freely (inline-override surface is the rule, not all `-c` flags)
|
|
144
|
+
'git -c http.proxy=http://proxy:8080 clone https://github.com/foo/bar',
|
|
145
|
+
'git -c core.autocrlf=false commit -m x',
|
|
146
|
+
'git -c init.defaultBranch=main init',
|
|
147
|
+
// The legitimate identity path: prefix every commit with `ADL_AGENT_ID_AGENT=agent-tarun` so the front controller stamps the right committer. This is what the rule tells the agent to do.
|
|
148
|
+
'ADL_AGENT_ID_AGENT=agent-tarun git commit -m "an agent commit"',
|
|
149
|
+
// The legitimate identity read: `git var GIT_AUTHOR_IDENT` (NOT `git config --get user.name`).
|
|
150
|
+
'git var GIT_AUTHOR_IDENT',
|
|
151
|
+
// `GIT_CONFIG_*` with no `user.*` key — not a user-override. Agents may legitimately override `http.proxy` or `init.defaultBranch` via the env-var form.
|
|
152
|
+
'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=http.proxy GIT_CONFIG_VALUE_0=http://proxy:8080 git clone https://github.com/foo/bar',
|
|
153
|
+
'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=init.defaultBranch GIT_CONFIG_VALUE_0=main git init',
|
|
154
|
+
// `GIT_CONFIG_KEY_<n>=user.*` with no COUNT or COUNT=0 — git ignores the key without a non-zero count, so this is a no-op and not a bypass.
|
|
155
|
+
'GIT_CONFIG_KEY_0=user.name GIT_CONFIG_VALUE_0=Foo git commit -m x',
|
|
156
|
+
'GIT_CONFIG_COUNT=0 GIT_CONFIG_KEY_0=user.name GIT_CONFIG_VALUE_0=Foo git commit -m x',
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
}
|
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
* `adl mcp cli pr-review --vote reject ...` reaches Azure DevOps exactly the
|
|
11
11
|
* way `mcp__azdo__pr_review` does, and must be denied the same way.
|
|
12
12
|
*
|
|
13
|
-
* Scoped to the
|
|
14
|
-
* `workitem-create-or-update
|
|
13
|
+
* Scoped to the commands this package's role rules actually gate —
|
|
14
|
+
* `workitem-create-or-update-feature`/`-pbi`/`-bug` (one command per MCP
|
|
15
|
+
* tool), `pr-create-or-update`, `pr-review`,
|
|
15
16
|
* `build-run` — not a general reimplementation of the whole CLI surface
|
|
16
17
|
* (`packages/azdo/src/cli.ts`'s `commands` table), which belongs to
|
|
17
18
|
* `packages/azdo`, outside this package's write scope. The flag → input-key
|
|
18
19
|
* mapping below is copied from that table (re-checked 2026-09-28) for
|
|
19
|
-
* exactly these
|
|
20
|
+
* exactly these entries; if `cli.ts`'s flags for one of them change,
|
|
20
21
|
* this table needs a matching update — there is no cross-package import that
|
|
21
22
|
* would keep the two in sync automatically without also pulling in
|
|
22
23
|
* `tool-dispatch.ts` and everything it depends on.
|
|
@@ -56,15 +57,18 @@ interface GatedCommand {
|
|
|
56
57
|
/**
|
|
57
58
|
* Only the flags a role rule actually inspects: `pr-create-or-update`'s
|
|
58
59
|
* `--action`/`--auto-complete`/`--no-auto-complete`/`--merge-strategy`/
|
|
59
|
-
* `--delete-source-branch` (`isPrCompletionCall`, pr-completion.ts)
|
|
60
|
+
* `--delete-source-branch` (`isPrCompletionCall`, pr-completion.ts),
|
|
60
61
|
* `pr-review`'s `--comment`/`--vote`/`--target-branch`
|
|
61
|
-
* (`adl-pr-review-role.ts`)
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* (`adl-pr-review-role.ts`), and `build-run`'s `--id`/`--branch`
|
|
63
|
+
* (`adl.build-run-role`). The three `workitem-create-or-update-*` commands
|
|
64
|
+
* are graded on identity
|
|
65
|
+
* alone — no input field changes its outcome — so their flag lists are empty;
|
|
66
|
+
* every command still needs an entry here so it is RECOGNIZED at all.
|
|
65
67
|
*/
|
|
66
68
|
const GATED_COMMANDS: Readonly<Record<string, GatedCommand>> = {
|
|
67
|
-
'workitem-create-or-update': { tool: '
|
|
69
|
+
'workitem-create-or-update-feature': { tool: 'workitem_create_or_update_feature', flags: [] },
|
|
70
|
+
'workitem-create-or-update-pbi': { tool: 'workitem_create_or_update_pbi', flags: [] },
|
|
71
|
+
'workitem-create-or-update-bug': { tool: 'workitem_create_or_update_bug', flags: [] },
|
|
68
72
|
'pr-create-or-update': {
|
|
69
73
|
tool: 'pr_create_or_update',
|
|
70
74
|
flags: [
|
|
@@ -83,14 +87,20 @@ const GATED_COMMANDS: Readonly<Record<string, GatedCommand>> = {
|
|
|
83
87
|
{ flag: '--target-branch', key: 'targetBranch', type: 'string' },
|
|
84
88
|
],
|
|
85
89
|
},
|
|
86
|
-
'build-run': {
|
|
90
|
+
'build-run': {
|
|
91
|
+
tool: 'build_run',
|
|
92
|
+
flags: [
|
|
93
|
+
{ flag: '--id', key: 'id', type: 'integer' },
|
|
94
|
+
{ flag: '--branch', key: 'branch', type: 'string' },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
/**
|
|
90
100
|
* `adl`/`adl.exe mcp cli <command> [--flag value|--boolean-flag ...]` → the
|
|
91
101
|
* MCP tool call it is equivalent to, or `null` when `inv` is not this shape,
|
|
92
102
|
* any of the first three words is dynamic, or `<command>` is not one of the
|
|
93
|
-
*
|
|
103
|
+
* six gated commands. A `--flag`'s value is read only when the flag is
|
|
94
104
|
* known for that specific command (an unrecognized flag is ignored, not an
|
|
95
105
|
* error — this is a read for GRADING, not the CLI's own arg parser, which
|
|
96
106
|
* would reject an unknown flag itself before any tool call happens) and the
|
|
@@ -156,7 +166,7 @@ export function toolCallFor(inv: Invocation): AdlMcpCliCall | null {
|
|
|
156
166
|
* `ADL_AGENT_ID_AGENT=` override on the very piece being graded wins. Grading
|
|
157
167
|
* the session identity alone would be wrong in both directions: a reviewer
|
|
158
168
|
* session's own inline `agent-tarun` override on
|
|
159
|
-
* `adl mcp cli workitem-create-or-update` would be denied as `agent-reviewer`
|
|
169
|
+
* `adl mcp cli workitem-create-or-update-bug` would be denied as `agent-reviewer`
|
|
160
170
|
* (a false deny), and — the more dangerous direction — an `agent-tarun`
|
|
161
171
|
* SESSION with an inline `ADL_AGENT_ID_AGENT=agent-reviewer` override would be
|
|
162
172
|
* graded as `agent-tarun` (a false ALLOW: the piece that will actually run is
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `adl.pr-complete-pending-decision` — docs/operations/agent-permissions.md row 9
|
|
3
|
-
* ("PR complete / set auto-complete"),
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* ("PR complete / set auto-complete"), DECIDED: denied for every identity —
|
|
4
|
+
* `agent-tarun`/`agent-vivek` AND `agent-reviewer` alike — as its own
|
|
5
|
+
* separately named rule specifically so the denial (or a future role split)
|
|
6
|
+
* can be relaxed without touching
|
|
7
7
|
* `adl.pr-create-update-role` (row 5), which keeps allowing a plain PR
|
|
8
8
|
* create/update for builders and `agent-reviewer` throughout
|
|
9
9
|
* (docs/specs/two-branch-standard-2026-09-27.md §1b.5: auto-complete is armed
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
* owner decision unrelated to this row, and denying that would block PR
|
|
17
17
|
* creation entirely).
|
|
18
18
|
*
|
|
19
|
-
* Deliberately does NOT require an identity to be present: row 9
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* Deliberately does NOT require an identity to be present: row 9 denies
|
|
20
|
+
* EVERY identity, including a caller with no session identity at all
|
|
21
|
+
* (whichever rule reports first, the outcome is the same: denied).
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import type { Invocation } from '../shell/invocation.ts'
|
|
@@ -36,7 +36,7 @@ export const adlPrCompletePendingDecision: RuleSpec = {
|
|
|
36
36
|
severity: 'deny',
|
|
37
37
|
enabledByDefault: true,
|
|
38
38
|
summary:
|
|
39
|
-
'Deny PR completion / auto-complete for every identity
|
|
39
|
+
'Deny PR completion / auto-complete for every identity (docs/operations/agent-permissions.md row 9) — the MCP tool AND its adl mcp cli equivalent.',
|
|
40
40
|
params: noParams,
|
|
41
41
|
match(inv: Invocation): Finding | null {
|
|
42
42
|
// `adl mcp cli
|
|
@@ -54,7 +54,7 @@ export const adlPrCompletePendingDecision: RuleSpec = {
|
|
|
54
54
|
return {
|
|
55
55
|
user,
|
|
56
56
|
agent:
|
|
57
|
-
`DENIED ${user} (docs/operations/agent-permissions.md row 9
|
|
57
|
+
`DENIED ${user} (docs/operations/agent-permissions.md row 9: denied for every identity). Create or update the PR's title/description ` +
|
|
58
58
|
`normally; do not pass autoComplete, mergeStrategy, or deleteSourceBranch. Blocked: ${ctx.snippet}`,
|
|
59
59
|
}
|
|
60
60
|
},
|