@lannguyensi/harness 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +17 -1
- package/dist/cli/doctor/format.js +24 -0
- package/dist/cli/doctor/format.js.map +1 -1
- package/dist/cli/doctor/index.d.ts +7 -0
- package/dist/cli/doctor/index.js +10 -0
- package/dist/cli/doctor/index.js.map +1 -1
- package/dist/cli/doctor/rogue-ledger.d.ts +25 -0
- package/dist/cli/doctor/rogue-ledger.js +106 -0
- package/dist/cli/doctor/rogue-ledger.js.map +1 -0
- package/dist/cli/doctor/types.d.ts +10 -1
- package/dist/cli/doctor/types.js.map +1 -1
- package/dist/cli/index.js +177 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/pack/hook-branch-protection.d.ts +30 -0
- package/dist/cli/pack/hook-branch-protection.js +279 -0
- package/dist/cli/pack/hook-branch-protection.js.map +1 -0
- package/dist/cli/pack/hook-codex-pre-tool-use.js +3 -1
- package/dist/cli/pack/hook-codex-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/hook-pre-tool-use.js +7 -2
- package/dist/cli/pack/hook-pre-tool-use.js.map +1 -1
- package/dist/cli/pack/understanding-report-schema-hint.d.ts +13 -0
- package/dist/cli/pack/understanding-report-schema-hint.js +54 -0
- package/dist/cli/pack/understanding-report-schema-hint.js.map +1 -0
- package/dist/cli/session-start/branch-check.d.ts +44 -0
- package/dist/cli/session-start/branch-check.js +165 -0
- package/dist/cli/session-start/branch-check.js.map +1 -0
- package/dist/cli/uninstall/index.d.ts +68 -0
- package/dist/cli/uninstall/index.js +586 -0
- package/dist/cli/uninstall/index.js.map +1 -0
- package/dist/cli/uninstall/snapshot.d.ts +40 -0
- package/dist/cli/uninstall/snapshot.js +34 -0
- package/dist/cli/uninstall/snapshot.js.map +1 -0
- package/dist/policy-packs/builtin/branch-protection-runtime.d.ts +47 -0
- package/dist/policy-packs/builtin/branch-protection-runtime.js +92 -0
- package/dist/policy-packs/builtin/branch-protection-runtime.js.map +1 -0
- package/dist/policy-packs/builtin/branch-protection.d.ts +9 -0
- package/dist/policy-packs/builtin/branch-protection.js +146 -0
- package/dist/policy-packs/builtin/branch-protection.js.map +1 -0
- package/dist/policy-packs/registry.d.ts +1 -1
- package/dist/policy-packs/registry.js +10 -3
- package/dist/policy-packs/registry.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Snapshot file format + path helpers for `harness uninstall --apply`.
|
|
2
|
+
//
|
|
3
|
+
// Sibling of `src/cli/gate/snapshot.ts`. A snapshot captures one
|
|
4
|
+
// uninstall invocation: which hook groups and which `mcpServers` entries
|
|
5
|
+
// were removed from `~/.claude/settings.json`, the sha256 of settings.json
|
|
6
|
+
// before/after, and the path the original was backed up to. Reading the
|
|
7
|
+
// snapshot is enough to manually reverse the uninstall (or to drive a
|
|
8
|
+
// future `harness reinstall` verb).
|
|
9
|
+
//
|
|
10
|
+
// The format mirrors `GateDisableSnapshot` shape but lives in its own
|
|
11
|
+
// file: the two verbs have different scopes (gate-disable is matcher-
|
|
12
|
+
// driven; uninstall is harness-ownership-driven) and storing them in the
|
|
13
|
+
// same snapshot stream would mean every reader has to disambiguate via
|
|
14
|
+
// `filter` or a discriminator. Cheaper to keep separate.
|
|
15
|
+
import * as crypto from "node:crypto";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
export const SNAPSHOT_BASENAME_PREFIX = "harness.uninstall.";
|
|
18
|
+
export const SNAPSHOT_BASENAME_SUFFIX = ".json";
|
|
19
|
+
export const BACKUP_INFIX = ".bak.uninstall.";
|
|
20
|
+
export const SNAPSHOT_VERSION = 1;
|
|
21
|
+
export function sha256Hex(input) {
|
|
22
|
+
return crypto.createHash("sha256").update(input).digest("hex");
|
|
23
|
+
}
|
|
24
|
+
/** Build the snapshot file path for `settingsPath` at `now`. */
|
|
25
|
+
export function snapshotPath(settingsPath, now) {
|
|
26
|
+
const stamp = now.toISOString().replace(/:/g, "-");
|
|
27
|
+
return path.join(path.dirname(settingsPath), `${SNAPSHOT_BASENAME_PREFIX}${stamp}${SNAPSHOT_BASENAME_SUFFIX}`);
|
|
28
|
+
}
|
|
29
|
+
/** Build the settings-backup file path for `settingsPath` at `now`. */
|
|
30
|
+
export function backupPath(settingsPath, now) {
|
|
31
|
+
const stamp = now.toISOString().replace(/:/g, "-");
|
|
32
|
+
return `${settingsPath}${BACKUP_INFIX}${stamp}`;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../../src/cli/uninstall/snapshot.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,EAAE;AACF,iEAAiE;AACjE,yEAAyE;AACzE,2EAA2E;AAC3E,wEAAwE;AACxE,sEAAsE;AACtE,oCAAoC;AACpC,EAAE;AACF,sEAAsE;AACtE,sEAAsE;AACtE,yEAAyE;AACzE,uEAAuE;AACvE,yDAAyD;AAEzD,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,MAAM,CAAC,MAAM,wBAAwB,GAAG,oBAAoB,CAAC;AAC7D,MAAM,CAAC,MAAM,wBAAwB,GAAG,OAAO,CAAC;AAChD,MAAM,CAAC,MAAM,YAAY,GAAG,iBAAiB,CAAC;AAC9C,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAU,CAAC;AAoC3C,MAAM,UAAU,SAAS,CAAC,KAAsB;IAC9C,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACjE,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,YAAY,CAAC,YAAoB,EAAE,GAAS;IAC1D,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACnD,OAAO,IAAI,CAAC,IAAI,CACd,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,EAC1B,GAAG,wBAAwB,GAAG,KAAK,GAAG,wBAAwB,EAAE,CACjE,CAAC;AACJ,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,UAAU,CAAC,YAAoB,EAAE,GAAS;IACxD,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACnD,OAAO,GAAG,YAAY,GAAG,YAAY,GAAG,KAAK,EAAE,CAAC;AAClD,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { PolicyPack } from "../../schema/index.js";
|
|
2
|
+
export declare const PACK_NAME = "branch-protection";
|
|
3
|
+
/**
|
|
4
|
+
* Ledger tag written by the producer when the current branch is NOT in
|
|
5
|
+
* the operator's protected list. The blocker substring-matches this
|
|
6
|
+
* prefix; the trailing `:<branch>` is informational and keeps the
|
|
7
|
+
* ledger row self-describing for auditors.
|
|
8
|
+
*/
|
|
9
|
+
export declare const NON_PROTECTED_TAG_PREFIX = "branch:non-protected";
|
|
10
|
+
/**
|
|
11
|
+
* Operator escape-hatch tag. Set via `mcp__agent-grounding__ledger_add`
|
|
12
|
+
* (Bash is gated by this very pack, so a shell-based override would be
|
|
13
|
+
* unreachable). The blocker substring-matches this prefix; the trailing
|
|
14
|
+
* `:<reason>` is a free-form note the operator types so a later audit
|
|
15
|
+
* can read WHY the override fired (e.g. `branch-protection-ack:hotfix
|
|
16
|
+
* for prod`).
|
|
17
|
+
*/
|
|
18
|
+
export declare const ACK_TAG_PREFIX = "branch-protection-ack";
|
|
19
|
+
/**
|
|
20
|
+
* Freshness window for the producer tag. Five minutes lets a single
|
|
21
|
+
* branch-check satisfy a whole edit batch without re-running for every
|
|
22
|
+
* Write; longer than that and a branch switch in the middle of a
|
|
23
|
+
* session would silently keep the gate open against the new HEAD.
|
|
24
|
+
*/
|
|
25
|
+
export declare const PRODUCER_FRESHNESS_MS: number;
|
|
26
|
+
/** Branches gated by default when no `config.protected_branches` is set. */
|
|
27
|
+
export declare const DEFAULT_PROTECTED_BRANCHES: readonly string[];
|
|
28
|
+
/**
|
|
29
|
+
* Parse the pack's `config.protected_branches` list. Falls back to the
|
|
30
|
+
* default allowlist when the operator hasn't customized it, OR when the
|
|
31
|
+
* provided value isn't a non-empty string array (the warning surfaces
|
|
32
|
+
* the type mismatch so the operator can fix it).
|
|
33
|
+
*
|
|
34
|
+
* Returns the resolved list plus a non-null warning message when the
|
|
35
|
+
* raw config was ill-formed. Caller appends the warning to the pack's
|
|
36
|
+
* `warnings` collection so it lands in apply output.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveProtectedBranches(pack: PolicyPack): {
|
|
39
|
+
branches: string[];
|
|
40
|
+
warning: string | null;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* True when `branch` is in the protected list. Empty branch (detached
|
|
44
|
+
* HEAD) is treated as protected, because we can't audit-by-name what
|
|
45
|
+
* the agent is about to commit to.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isProtectedBranch(branch: string, protectedList: readonly string[]): boolean;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Shared runtime constants + helpers for the `branch-protection` policy pack.
|
|
2
|
+
//
|
|
3
|
+
// The pack itself (`branch-protection.ts`) only emits hooks + the
|
|
4
|
+
// audit-copy instructions. The actual enforcement lives in two CLI verbs
|
|
5
|
+
// — `harness session-start branch-check` (producer) and `harness pack
|
|
6
|
+
// hook branch-protection` (blocker) — both under `src/cli/`. This module
|
|
7
|
+
// is the small shared surface they pull from: tag formats, default
|
|
8
|
+
// protected list, config parsing.
|
|
9
|
+
export const PACK_NAME = "branch-protection";
|
|
10
|
+
/**
|
|
11
|
+
* Ledger tag written by the producer when the current branch is NOT in
|
|
12
|
+
* the operator's protected list. The blocker substring-matches this
|
|
13
|
+
* prefix; the trailing `:<branch>` is informational and keeps the
|
|
14
|
+
* ledger row self-describing for auditors.
|
|
15
|
+
*/
|
|
16
|
+
export const NON_PROTECTED_TAG_PREFIX = "branch:non-protected";
|
|
17
|
+
/**
|
|
18
|
+
* Operator escape-hatch tag. Set via `mcp__agent-grounding__ledger_add`
|
|
19
|
+
* (Bash is gated by this very pack, so a shell-based override would be
|
|
20
|
+
* unreachable). The blocker substring-matches this prefix; the trailing
|
|
21
|
+
* `:<reason>` is a free-form note the operator types so a later audit
|
|
22
|
+
* can read WHY the override fired (e.g. `branch-protection-ack:hotfix
|
|
23
|
+
* for prod`).
|
|
24
|
+
*/
|
|
25
|
+
export const ACK_TAG_PREFIX = "branch-protection-ack";
|
|
26
|
+
/**
|
|
27
|
+
* Freshness window for the producer tag. Five minutes lets a single
|
|
28
|
+
* branch-check satisfy a whole edit batch without re-running for every
|
|
29
|
+
* Write; longer than that and a branch switch in the middle of a
|
|
30
|
+
* session would silently keep the gate open against the new HEAD.
|
|
31
|
+
*/
|
|
32
|
+
export const PRODUCER_FRESHNESS_MS = 5 * 60 * 1000;
|
|
33
|
+
/** Branches gated by default when no `config.protected_branches` is set. */
|
|
34
|
+
export const DEFAULT_PROTECTED_BRANCHES = [
|
|
35
|
+
"master",
|
|
36
|
+
"main",
|
|
37
|
+
"develop",
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Parse the pack's `config.protected_branches` list. Falls back to the
|
|
41
|
+
* default allowlist when the operator hasn't customized it, OR when the
|
|
42
|
+
* provided value isn't a non-empty string array (the warning surfaces
|
|
43
|
+
* the type mismatch so the operator can fix it).
|
|
44
|
+
*
|
|
45
|
+
* Returns the resolved list plus a non-null warning message when the
|
|
46
|
+
* raw config was ill-formed. Caller appends the warning to the pack's
|
|
47
|
+
* `warnings` collection so it lands in apply output.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveProtectedBranches(pack) {
|
|
50
|
+
const raw = pack.config["protected_branches"];
|
|
51
|
+
if (raw === undefined) {
|
|
52
|
+
return { branches: [...DEFAULT_PROTECTED_BRANCHES], warning: null };
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(raw)) {
|
|
55
|
+
return {
|
|
56
|
+
branches: [...DEFAULT_PROTECTED_BRANCHES],
|
|
57
|
+
warning: `policy_packs[${pack.name}].config.protected_branches: expected an array of strings, got ${typeof raw}; falling back to defaults (${DEFAULT_PROTECTED_BRANCHES.join(", ")}).`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const ok = [];
|
|
61
|
+
const bad = [];
|
|
62
|
+
for (const entry of raw) {
|
|
63
|
+
if (typeof entry === "string" && entry.length > 0)
|
|
64
|
+
ok.push(entry);
|
|
65
|
+
else
|
|
66
|
+
bad.push(entry);
|
|
67
|
+
}
|
|
68
|
+
if (ok.length === 0) {
|
|
69
|
+
return {
|
|
70
|
+
branches: [...DEFAULT_PROTECTED_BRANCHES],
|
|
71
|
+
warning: `policy_packs[${pack.name}].config.protected_branches: every entry was rejected (need non-empty strings); falling back to defaults (${DEFAULT_PROTECTED_BRANCHES.join(", ")}).`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (bad.length > 0) {
|
|
75
|
+
return {
|
|
76
|
+
branches: ok,
|
|
77
|
+
warning: `policy_packs[${pack.name}].config.protected_branches: skipped ${bad.length} non-string entr${bad.length === 1 ? "y" : "ies"}; using ${ok.length} valid one${ok.length === 1 ? "" : "s"} (${ok.join(", ")}).`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { branches: ok, warning: null };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* True when `branch` is in the protected list. Empty branch (detached
|
|
84
|
+
* HEAD) is treated as protected, because we can't audit-by-name what
|
|
85
|
+
* the agent is about to commit to.
|
|
86
|
+
*/
|
|
87
|
+
export function isProtectedBranch(branch, protectedList) {
|
|
88
|
+
if (branch.length === 0)
|
|
89
|
+
return true;
|
|
90
|
+
return protectedList.includes(branch);
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=branch-protection-runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branch-protection-runtime.js","sourceRoot":"","sources":["../../../src/policy-packs/builtin/branch-protection-runtime.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,EAAE;AACF,kEAAkE;AAClE,yEAAyE;AACzE,sEAAsE;AACtE,yEAAyE;AACzE,mEAAmE;AACnE,kCAAkC;AAIlC,MAAM,CAAC,MAAM,SAAS,GAAG,mBAAmB,CAAC;AAE7C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,sBAAsB,CAAC;AAE/D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,uBAAuB,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAEnD,4EAA4E;AAC5E,MAAM,CAAC,MAAM,0BAA0B,GAAsB;IAC3D,QAAQ;IACR,MAAM;IACN,SAAS;CACV,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAgB;IAIvD,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAC9C,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,0BAA0B,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACtE,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,QAAQ,EAAE,CAAC,GAAG,0BAA0B,CAAC;YACzC,OAAO,EAAE,gBAAgB,IAAI,CAAC,IAAI,kEAAkE,OAAO,GAAG,+BAA+B,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;SACvL,CAAC;IACJ,CAAC;IACD,MAAM,EAAE,GAAa,EAAE,CAAC;IACxB,MAAM,GAAG,GAAc,EAAE,CAAC;IAC1B,KAAK,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QACxB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;;YAC7D,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IACD,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpB,OAAO;YACL,QAAQ,EAAE,CAAC,GAAG,0BAA0B,CAAC;YACzC,OAAO,EAAE,gBAAgB,IAAI,CAAC,IAAI,6GAA6G,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;SACzL,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO;YACL,QAAQ,EAAE,EAAE;YACZ,OAAO,EAAE,gBAAgB,IAAI,CAAC,IAAI,wCAAwC,GAAG,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,WAAW,EAAE,CAAC,MAAM,aAAa,EAAE,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;SACvN,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AACzC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc,EAAE,aAAgC;IAChF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACxC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PolicyPack } from "../../schema/index.js";
|
|
2
|
+
import { type Runtime } from "../runtime.js";
|
|
3
|
+
import type { PackContribution } from "../types.js";
|
|
4
|
+
import { PACK_NAME } from "./branch-protection-runtime.js";
|
|
5
|
+
export { PACK_NAME };
|
|
6
|
+
export declare function resolve(pack: PolicyPack, runtime?: Runtime): {
|
|
7
|
+
contribution: PackContribution;
|
|
8
|
+
warnings: string[];
|
|
9
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Builtin Policy Pack: `branch-protection`.
|
|
2
|
+
//
|
|
3
|
+
// Blocks Write/Edit (and the codex `apply_patch` equivalent) when the
|
|
4
|
+
// agent is on a protected branch (default: master, main, develop). The
|
|
5
|
+
// gate fires at the FIRST source mutation, complementing the existing
|
|
6
|
+
// `preflight-before-push` gate which fires at the LAST reversible step.
|
|
7
|
+
//
|
|
8
|
+
// Mechanics, mirroring `understanding-before-execution`:
|
|
9
|
+
//
|
|
10
|
+
// 1. SessionStart producer (`harness session-start branch-check`) reads
|
|
11
|
+
// `.git/HEAD` for the cwd and, if the branch is NOT protected,
|
|
12
|
+
// writes a `branch:non-protected:<branch>` fact to the evidence
|
|
13
|
+
// ledger for the current session.
|
|
14
|
+
//
|
|
15
|
+
// 2. PreToolUse blocker (`harness pack hook branch-protection`)
|
|
16
|
+
// consults the ledger on every Write/Edit (or `apply_patch`) and
|
|
17
|
+
// emits a Claude Code deny envelope unless either:
|
|
18
|
+
// - a fresh (<5m) `branch:non-protected` tag exists, OR
|
|
19
|
+
// - a `branch-protection-ack:` override tag exists (any age,
|
|
20
|
+
// written by the operator via `mcp__agent-grounding__ledger_add`
|
|
21
|
+
// since Bash is gated by this same pack).
|
|
22
|
+
//
|
|
23
|
+
// The producer is also runnable on-demand from the operator's `!` shell
|
|
24
|
+
// — same CLI verb, no SessionStart event piped on stdin — so an agent
|
|
25
|
+
// that just branched can refresh the gate without restarting the
|
|
26
|
+
// session.
|
|
27
|
+
//
|
|
28
|
+
// Pack is OFF by default: it must be enabled per-installation via
|
|
29
|
+
// `harness pack add branch-protection`. The `full` init template does
|
|
30
|
+
// NOT wire it (revisit after one cycle of operator feedback).
|
|
31
|
+
import { DEFAULT_RUNTIME } from "../runtime.js";
|
|
32
|
+
import { ACK_TAG_PREFIX, DEFAULT_PROTECTED_BRANCHES, NON_PROTECTED_TAG_PREFIX, PACK_NAME, PRODUCER_FRESHNESS_MS, resolveProtectedBranches, } from "./branch-protection-runtime.js";
|
|
33
|
+
export { PACK_NAME };
|
|
34
|
+
const HOOK_NAME_PREFIX = `policy-pack:${PACK_NAME}`;
|
|
35
|
+
const PRE_TOOL_USE_MATCH_CLAUDE = "Write|Edit";
|
|
36
|
+
const PRE_TOOL_USE_MATCH_CODEX = "apply_patch";
|
|
37
|
+
const PRODUCER_COMMAND = "harness session-start branch-check";
|
|
38
|
+
const BLOCKER_COMMAND = "harness pack hook branch-protection";
|
|
39
|
+
function buildHooks(runtime) {
|
|
40
|
+
const isCodex = runtime === "codex";
|
|
41
|
+
const blockerMatch = isCodex ? PRE_TOOL_USE_MATCH_CODEX : PRE_TOOL_USE_MATCH_CLAUDE;
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
name: `${HOOK_NAME_PREFIX}:session-start`,
|
|
45
|
+
event: "SessionStart",
|
|
46
|
+
command: PRODUCER_COMMAND,
|
|
47
|
+
blocking: false,
|
|
48
|
+
budget_ms: 5000,
|
|
49
|
+
description: "Producer: write `branch:non-protected:<branch>` to the evidence ledger when the session opens on a non-protected branch. Non-blocking; failures leave the gate closed.",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: `${HOOK_NAME_PREFIX}:pre-tool-use`,
|
|
53
|
+
event: "PreToolUse",
|
|
54
|
+
match: blockerMatch,
|
|
55
|
+
command: BLOCKER_COMMAND,
|
|
56
|
+
blocking: "hard",
|
|
57
|
+
budget_ms: 5000,
|
|
58
|
+
description: `Blocker: deny ${blockerMatch} on protected branches unless a fresh branch:non-protected tag or a branch-protection-ack override exists in the ledger.`,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
function buildInstructions(pack, branches, runtime) {
|
|
63
|
+
const description = pack.description?.trim() ?? "";
|
|
64
|
+
const isCodex = runtime === "codex";
|
|
65
|
+
const blockerMatch = isCodex ? PRE_TOOL_USE_MATCH_CODEX : PRE_TOOL_USE_MATCH_CLAUDE;
|
|
66
|
+
const settingsArtefact = isCodex
|
|
67
|
+
? "`harness.generated/codex/config.toml`"
|
|
68
|
+
: "harness-managed `settings.json`";
|
|
69
|
+
const minutes = Math.round(PRODUCER_FRESHNESS_MS / 60000);
|
|
70
|
+
return `# Policy Pack: ${PACK_NAME}
|
|
71
|
+
|
|
72
|
+
> Operator audit copy. This pack blocks source-mutating tool calls when
|
|
73
|
+
> the agent is on a protected branch, closing the loop on the
|
|
74
|
+
> "edit-on-master" incident pattern.
|
|
75
|
+
|
|
76
|
+
## Runtime
|
|
77
|
+
|
|
78
|
+
${runtime}
|
|
79
|
+
|
|
80
|
+
## Protected branches
|
|
81
|
+
|
|
82
|
+
${branches.map((b) => `- \`${b}\``).join("\n")}
|
|
83
|
+
|
|
84
|
+
Set \`config.protected_branches\` in your manifest to override.
|
|
85
|
+
|
|
86
|
+
## Effect
|
|
87
|
+
|
|
88
|
+
While this pack is enabled, hooks are wired into the ${settingsArtefact}:
|
|
89
|
+
|
|
90
|
+
1. \`SessionStart\` producer (\`${PRODUCER_COMMAND}\`, blocking: false):
|
|
91
|
+
reads the cwd's \`.git/HEAD\`. If the branch is NOT in the protected
|
|
92
|
+
list, writes \`${NON_PROTECTED_TAG_PREFIX}:<branch>\` to the evidence
|
|
93
|
+
ledger for the current session.
|
|
94
|
+
|
|
95
|
+
2. \`PreToolUse\` blocker (\`${BLOCKER_COMMAND}\`, blocking: hard) on
|
|
96
|
+
\`${blockerMatch}\`: refuses the tool call unless EITHER
|
|
97
|
+
- a \`${NON_PROTECTED_TAG_PREFIX}\` tag exists in the ledger from
|
|
98
|
+
within the last ${minutes} minutes, OR
|
|
99
|
+
- a \`${ACK_TAG_PREFIX}:<reason>\` override tag exists (any age).
|
|
100
|
+
|
|
101
|
+
## Escape hatches
|
|
102
|
+
|
|
103
|
+
- **Refresh after branching**: the producer is runnable on demand from
|
|
104
|
+
the operator's \`!\` shell as \`${PRODUCER_COMMAND}\`. The agent's Bash
|
|
105
|
+
is gated by the Understanding Gate but the producer command is itself
|
|
106
|
+
a \`harness ...\` invocation that the gate's allowlist accepts.
|
|
107
|
+
|
|
108
|
+
- **Explicit override** (any age, lasts the session): write the ack tag
|
|
109
|
+
via \`mcp__agent-grounding__ledger_add\` with
|
|
110
|
+
\`content: "${ACK_TAG_PREFIX}:<reason>"\`. Use this when you have a
|
|
111
|
+
deliberate reason to edit a protected branch — version bumps, CI
|
|
112
|
+
workflow patches, etc. The override survives session restarts only as
|
|
113
|
+
long as the ledger row does.
|
|
114
|
+
|
|
115
|
+
## Out of scope (v1)
|
|
116
|
+
|
|
117
|
+
- Locking down \`git\` itself (would create false-positive churn on
|
|
118
|
+
read-only commands like \`git status\`).
|
|
119
|
+
- Auto-branching on Write attempt (silent autocorrect is wrong; the
|
|
120
|
+
agent should be the one who notices and branches).
|
|
121
|
+
- Path-allowlist for safe-on-master files (CHANGELOG.md, version
|
|
122
|
+
bumps). Open for v2 if operators report friction.
|
|
123
|
+
|
|
124
|
+
## Pack metadata
|
|
125
|
+
${description ? `\n> ${description.replace(/\n/g, "\n> ")}\n` : ""}
|
|
126
|
+
- Source: \`builtin\`
|
|
127
|
+
- Pack: \`${PACK_NAME}\`
|
|
128
|
+
- Runtime: \`${runtime}\`
|
|
129
|
+
- Defaults: ${DEFAULT_PROTECTED_BRANCHES.join(", ")}
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
export function resolve(pack, runtime = DEFAULT_RUNTIME) {
|
|
133
|
+
const { branches, warning } = resolveProtectedBranches(pack);
|
|
134
|
+
const hooks = buildHooks(runtime);
|
|
135
|
+
const files = [
|
|
136
|
+
{
|
|
137
|
+
relativePath: `policy-packs/${PACK_NAME}/instructions.md`,
|
|
138
|
+
content: buildInstructions(pack, branches, runtime),
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
const warnings = [];
|
|
142
|
+
if (warning)
|
|
143
|
+
warnings.push(warning);
|
|
144
|
+
return { contribution: { hooks, files }, warnings };
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=branch-protection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branch-protection.js","sourceRoot":"","sources":["../../../src/policy-packs/builtin/branch-protection.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import type { PolicyPack } from "../schema/index.js";
|
|
|
2
2
|
import { type ResolvePackOptions } from "./builtin/understanding-before-execution.js";
|
|
3
3
|
import { type Runtime } from "./runtime.js";
|
|
4
4
|
import type { PackContribution } from "./types.js";
|
|
5
|
-
export declare const KNOWN_BUILTIN_PACKS: readonly ["understanding-before-execution"];
|
|
5
|
+
export declare const KNOWN_BUILTIN_PACKS: readonly ["understanding-before-execution", "branch-protection"];
|
|
6
6
|
export type BuiltinPackName = (typeof KNOWN_BUILTIN_PACKS)[number];
|
|
7
7
|
export declare function isBuiltinPackName(name: string): name is BuiltinPackName;
|
|
8
8
|
export interface ResolveBuiltinResult {
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
// Registry of builtin policy-pack names.
|
|
2
2
|
//
|
|
3
|
-
// Phase 6 #2
|
|
4
|
-
//
|
|
3
|
+
// Phase 6 #2 shipped `understanding-before-execution`; subsequent
|
|
4
|
+
// builtins are added by appending to `KNOWN_BUILTIN_PACKS` and a case
|
|
5
|
+
// arm in `resolveBuiltin()`. Non-builtin sources (path/npm/git) are
|
|
5
6
|
// out of scope for v1; their resolution lands in a later sub-task.
|
|
7
|
+
import { PACK_NAME as BRANCH_PROTECTION, resolve as resolveBranchProtection, } from "./builtin/branch-protection.js";
|
|
6
8
|
import { PACK_NAME as UNDERSTANDING_BEFORE_EXECUTION, resolve as resolveUnderstandingBeforeExecution, } from "./builtin/understanding-before-execution.js";
|
|
7
9
|
import { DEFAULT_RUNTIME } from "./runtime.js";
|
|
8
|
-
export const KNOWN_BUILTIN_PACKS = [
|
|
10
|
+
export const KNOWN_BUILTIN_PACKS = [
|
|
11
|
+
UNDERSTANDING_BEFORE_EXECUTION,
|
|
12
|
+
BRANCH_PROTECTION,
|
|
13
|
+
];
|
|
9
14
|
export function isBuiltinPackName(name) {
|
|
10
15
|
return KNOWN_BUILTIN_PACKS.includes(name);
|
|
11
16
|
}
|
|
@@ -15,6 +20,8 @@ export function resolveBuiltin(pack, runtime = DEFAULT_RUNTIME, opts = {}) {
|
|
|
15
20
|
switch (pack.name) {
|
|
16
21
|
case UNDERSTANDING_BEFORE_EXECUTION:
|
|
17
22
|
return resolveUnderstandingBeforeExecution(pack, runtime, opts);
|
|
23
|
+
case BRANCH_PROTECTION:
|
|
24
|
+
return resolveBranchProtection(pack, runtime);
|
|
18
25
|
}
|
|
19
26
|
}
|
|
20
27
|
//# sourceMappingURL=registry.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/policy-packs/registry.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/policy-packs/registry.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,kEAAkE;AAClE,sEAAsE;AACtE,oEAAoE;AACpE,mEAAmE;AAGnE,OAAO,EACL,SAAS,IAAI,iBAAiB,EAC9B,OAAO,IAAI,uBAAuB,GACnC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,SAAS,IAAI,8BAA8B,EAC3C,OAAO,IAAI,mCAAmC,GAE/C,MAAM,6CAA6C,CAAC;AACrD,OAAO,EAAE,eAAe,EAAgB,MAAM,cAAc,CAAC;AAG7D,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,8BAA8B;IAC9B,iBAAiB;CACT,CAAC;AAGX,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAQ,mBAAyC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AACnE,CAAC;AAOD,MAAM,UAAU,cAAc,CAC5B,IAAgB,EAChB,UAAmB,eAAe,EAClC,OAA2B,EAAE;IAE7B,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/C,QAAQ,IAAI,CAAC,IAAuB,EAAE,CAAC;QACrC,KAAK,8BAA8B;YACjC,OAAO,mCAAmC,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;QAClE,KAAK,iBAAiB;YACpB,OAAO,uBAAuB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAClD,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lannguyensi/harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Declarative control plane for agent harnesses — one YAML for grounding, tools, memory, and hooks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/LanNguyenSi/harness",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"test:watch": "vitest",
|
|
38
38
|
"test:cov": "vitest run --coverage",
|
|
39
39
|
"typecheck": "tsc --noEmit",
|
|
40
|
+
"check:ug-schema-drift": "node scripts/check-ug-schema-drift.mjs",
|
|
40
41
|
"prepublishOnly": "npm run build"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|