@tekmidian/pai 1.0.4 → 1.0.6
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/.claude-plugin/plugin.json +1 -1
- package/dist/{aibroker-client-CQPGqLxL.mjs → aibroker-client-CXsyC7A8.mjs} +2 -2
- package/dist/aibroker-client-ChB7gSxV.mjs +3 -0
- package/dist/auto-route-Bc8xP6TQ.mjs +11 -0
- package/dist/{auto-route-21GdHL70.mjs → auto-route-YaEGVrFl.mjs} +2 -2
- package/dist/budget-DEiebT2A.mjs +15 -0
- package/dist/{caller-identity-yLFb7CKq.mjs → caller-identity-C6Nc96iM.mjs} +4 -4
- package/dist/{capacity-DEveQdFV.mjs → capacity-B2PgKAg4.mjs} +69 -6
- package/dist/{chain-D7PFur8Y.mjs → chain-CtK49-Ma.mjs} +5 -5
- package/dist/cli/index.mjs +2126 -290
- package/dist/{cockpit-actions-BfRMr2sm.mjs → cockpit-actions-C73mJAJ8.mjs} +18 -18
- package/dist/{cockpit-actions-OodH_vx_.mjs → cockpit-actions-D1OPP6Es.mjs} +30 -26
- package/dist/cockpit-memory-CLHZD64W.mjs +20 -0
- package/dist/config/defaults/config.yaml +21 -1
- package/dist/{config-XpZWOYOG.mjs → config-Bz8pAU5Q.mjs} +30 -5
- package/dist/{config-DtENJ6cL.mjs → config-DHgPx3ER.mjs} +1 -1
- package/dist/config-DLmyEK2q.mjs +4 -0
- package/dist/{config-Bu1_vbY8.mjs → config-SlZ-Z52i.mjs} +4 -3
- package/dist/{controls-JLm6g0yP.mjs → controls-rIItFeIJ.mjs} +59 -66
- package/dist/daemon/index.mjs +40 -39
- package/dist/daemon-176qA79N.mjs +46 -0
- package/dist/{daemon-nXanenDM.mjs → daemon-SoUvPUOf.mjs} +80 -30
- package/dist/daemon-mcp/index.mjs +49 -38
- package/dist/detect-C8nFP-kR.mjs +10 -0
- package/dist/{detect-BLKsUOgG.mjs → detect-DLjeYwnt.mjs} +2 -2
- package/dist/{embed-pass-e64AwDCa.mjs → embed-pass-CN1D8MXl.mjs} +5 -5
- package/dist/{embedding-gate-Cnk4YCyX.mjs → embedding-gate-BVQvUmox.mjs} +2 -2
- package/dist/{embedding-gate-6rZrWRXa.mjs → embedding-gate-iq0oQEkF.mjs} +3 -3
- package/dist/{endreason-BC-v05H_.mjs → endreason-C2-hYK0_.mjs} +4 -4
- package/dist/{factory-B7nv75rC.mjs → factory-CYwa0jYj.mjs} +6 -6
- package/dist/factory-DFwez99M.mjs +9 -0
- package/dist/{fallback-CBWpp2F2.mjs → fallback-CwPSCrX8.mjs} +9 -231
- package/dist/{health-C4FQmkJn.mjs → health-D0mUAVdw.mjs} +5 -5
- package/dist/{helpers-DSNK_1KP.mjs → helpers-Dj2nY56t.mjs} +2 -2
- package/dist/{helpers-Dz7mjTSw.mjs → helpers-DyHKAwrW.mjs} +6 -6
- package/dist/hooks/block-sleep-poll.mjs +1 -1
- package/dist/hooks/capture-all-events.mjs +68 -3
- package/dist/hooks/capture-tool-output.mjs +68 -3
- package/dist/hooks/handoff-link.mjs +128 -0
- package/dist/hooks/load-project-context.mjs +8 -8
- package/dist/hooks/local-model-hint.mjs +1 -1
- package/dist/hooks/route-agents-to-worker.mjs +1 -1
- package/dist/hooks/security-validator.mjs +842 -90
- package/dist/hooks/supervision-banner.mjs +186 -48
- package/dist/hooks/worker-edit-repair.mjs +2 -2
- package/dist/hooks/worker-guard.mjs +65 -9
- package/dist/hooks/worker-note.mjs +70 -5
- package/dist/hooks/worker-pool-host.mjs +1518 -265
- package/dist/hooks/worker-proxy.mjs +1 -1
- package/dist/hooks/worker-status-line.mjs +225 -86
- package/dist/hooks/worker-supervision.mjs +1 -1
- package/dist/hooks/worker-tool-name-repair.mjs +2 -2
- package/dist/index.mjs +9 -9
- package/dist/{ipc-client-CnYEF31R.mjs → ipc-client-CmagIFLh.mjs} +2 -2
- package/dist/{ipc-client-DN7EvoVo.mjs → ipc-client-DmbSIBUK.mjs} +1 -1
- package/dist/{kill-tree-DleMNZX3.mjs → kill-tree-DPiMscaY.mjs} +8 -8
- package/dist/kill-tree-Dxei6qlR.mjs +10 -0
- package/dist/{launch-DlZTKE3u.mjs → launch-BZT3JDoZ.mjs} +3 -3
- package/dist/{launch-Di7N-J5X.mjs → launch-Bn2Zf90h.mjs} +3 -3
- package/dist/{indexer-backend-BdjeL4P8.mjs → lessons-CFVU0cwA.mjs} +262 -43
- package/dist/{local-candidate-DLdwQt0s.mjs → local-candidate-DFd4pp-w.mjs} +3 -3
- package/dist/{local-instances-DOxSgT_L.mjs → local-instances-DhF4_sV2.mjs} +2 -2
- package/dist/{local-instances-ZeRkkuS5.mjs → local-instances-KTh8IeOT.mjs} +4 -4
- package/dist/{local-tiers-BE4jGaMa.mjs → local-tiers-CqwGUcEn.mjs} +3 -3
- package/dist/{local-tiers-DNMI5W86.mjs → local-tiers-DSlIYvvm.mjs} +3 -3
- package/dist/{main-config-ops-kTXR8Pyv.mjs → main-config-ops-rda08BCD.mjs} +3 -3
- package/dist/{main-resolver-yp_puSSb.mjs → main-resolver-BKwI0I6H.mjs} +18 -18
- package/dist/{migrate-DrA_OwlZ.mjs → migrate-C2z1YmWC.mjs} +2 -2
- package/dist/{model-inventory-h6Z9fN_o.mjs → model-inventory-BXv15B0d.mjs} +13 -13
- package/dist/{models-add-TVni3A7V.mjs → models-add-5yXTyz3g.mjs} +38 -37
- package/dist/operator-Bmv5tiHQ.mjs +7 -0
- package/dist/{operator-CyuB3aVw.mjs → operator-Br6GeswF.mjs} +2 -2
- package/dist/{pai-marker-BcZyJguM.mjs → pai-marker-D6OoVbM_.mjs} +2 -2
- package/dist/{pane-audit-Lqy9N_Ki.mjs → pane-audit-DLYy9-4g.mjs} +13 -23
- package/dist/{paths-CHifubDh.mjs → paths-CypxFTfn.mjs} +4 -4
- package/dist/{paths-CFHTloSe.mjs → paths-DIOwNcj-.mjs} +3 -3
- package/dist/{planner-j77ndkBf.mjs → planner-BzZ_2lc-.mjs} +29 -29
- package/dist/{pool-Dh-q3iJi.mjs → pool-9EsbKD-U.mjs} +2 -2
- package/dist/{pool-cli-DQ6OzRy2.mjs → pool-cli-Cy5mlycP.mjs} +6 -6
- package/dist/{pool-cli-BggU8GRp.mjs → pool-cli-Db0uDwB9.mjs} +5 -5
- package/dist/{postgres-BK4FP9Zg.mjs → postgres-B1B80bcj.mjs} +3 -3
- package/dist/{postgres-DNIOQijl.mjs → postgres-BL5oBBgY.mjs} +3 -3
- package/dist/{project-setup-CBRtFFh0.mjs → project-setup-DHxdJjEc.mjs} +24 -25
- package/dist/providers-BH6hQYbU.mjs +16 -0
- package/dist/{providers-C9QC0TtO.mjs → providers-BNV7CzC-.mjs} +6 -6
- package/dist/{query-feedback-DN52N9C5.mjs → query-feedback-BWl2HRwN.mjs} +2 -2
- package/dist/registrable-BwFiqazy.mjs +9 -0
- package/dist/{registrable-DpyTtajg.mjs → registrable-Dxs4oKIj.mjs} +4 -4
- package/dist/render-DVGXpfaT.mjs +13 -0
- package/dist/render-jPpMrudf.mjs +2240 -0
- package/dist/root-C8cxhdn9.mjs +36 -0
- package/dist/{root-C4oXlXS_.mjs → root-DmOzMeSv.mjs} +10 -10
- package/dist/route-table-COAmc1nB.mjs +15 -0
- package/dist/{router-CQSc3lk3.mjs → router-D2476pOp.mjs} +2 -2
- package/dist/{router-DwHJRgG9.mjs → router-ghujlEFb.mjs} +1 -1
- package/dist/{routing-f2zulGwD.mjs → routing-B_cg1_Xx.mjs} +13 -13
- package/dist/{routing-DmkheCRH.mjs → routing-aJKMIagQ.mjs} +16 -16
- package/dist/{run-BwZKBJZZ.mjs → run-DE9Q0ldN.mjs} +55 -31
- package/dist/{runtime-paths-WsO9sJFc.mjs → runtime-paths-twfV23YL.mjs} +2 -2
- package/dist/{scan-BBGvU5EB.mjs → scan-CNMcltOX.mjs} +7 -7
- package/dist/{scope-D-h6F6DN.mjs → scope-BTWstJ1B.mjs} +4 -4
- package/dist/session-DgZifjLx.mjs +35 -0
- package/dist/{session-Dn5OJXlg.mjs → session-HBYOABWU.mjs} +28 -28
- package/dist/session-scan-BZxnYN1v.mjs +9 -0
- package/dist/{session-scan-DUn10MBd.mjs → session-scan-CX3MKCNg.mjs} +6 -6
- package/dist/{session-usage-CPvXvSkw.mjs → session-usage-CCmPFFa6.mjs} +117 -2
- package/dist/{status-ggJ7lgVg.mjs → status-Cecau8Ps.mjs} +9 -5
- package/dist/status-D52xC_i0.mjs +6 -0
- package/dist/{tool-errors-7E13StWd.mjs → tool-errors-BFw1gv-z.mjs} +2 -2
- package/dist/{tool-errors-CJWODvlo.mjs → tool-errors-COM2JGEr.mjs} +6 -6
- package/dist/viewer-Ce-Xa9L3.mjs +22 -0
- package/dist/{viewer-wfKnvnrI.mjs → viewer-DvBg6N22.mjs} +16 -14
- package/dist/{wait-ByV0OyMZ.mjs → wait-C5iWILj_.mjs} +3 -3
- package/dist/worker-config-BmcJ9R0U.mjs +42 -0
- package/dist/{worker-config-eY5Qy4vl.mjs → worker-config-DkONNk_H.mjs} +164 -107
- package/dist/workers-config-DwzrtdYu.mjs +4 -0
- package/dist/{worktree-CRs2gyg0.mjs → worktree-O_kU6Q3y.mjs} +164 -8
- package/dist/{zettelkasten-tpCRBYvH.mjs → zettelkasten-BH8AdNh8.mjs} +4 -4
- package/dist/{zettelkasten-_h_tUm2T.mjs → zettelkasten-FFhQFESJ.mjs} +2 -2
- package/docs/commands/README.md +33 -2
- package/docs/commands/audit.md +34 -3
- package/docs/commands/backup.md +1 -1
- package/docs/commands/build.md +4 -4
- package/docs/commands/cd.md +1 -1
- package/docs/commands/clear-names.md +1 -1
- package/docs/commands/cockpit.md +1 -1
- package/docs/commands/config.md +21 -1
- package/docs/commands/daemon.md +1 -1
- package/docs/commands/db.md +1 -1
- package/docs/commands/doc.md +1 -1
- package/docs/commands/end.md +1 -1
- package/docs/commands/find.md +1 -1
- package/docs/commands/git.md +110 -0
- package/docs/commands/handoff.md +70 -0
- package/docs/commands/harness.md +74 -0
- package/docs/commands/help.md +1 -1
- package/docs/commands/home.md +1 -1
- package/docs/commands/hooks-db.md +1 -1
- package/docs/commands/identity.md +1 -1
- package/docs/commands/install.md +1 -1
- package/docs/commands/issue.md +1 -1
- package/docs/commands/kg.md +1 -1
- package/docs/commands/launch.md +1 -1
- package/docs/commands/lessons.md +201 -0
- package/docs/commands/list.md +1 -1
- package/docs/commands/ls.md +1 -1
- package/docs/commands/mcp.md +1 -1
- package/docs/commands/memory.md +1 -1
- package/docs/commands/models.md +1 -1
- package/docs/commands/more.md +1 -1
- package/docs/commands/new.md +1 -1
- package/docs/commands/notes.md +1 -1
- package/docs/commands/notify.md +1 -1
- package/docs/commands/observation.md +1 -1
- package/docs/commands/obsidian.md +1 -1
- package/docs/commands/open.md +1 -1
- package/docs/commands/pane.md +1 -1
- package/docs/commands/pause.md +1 -1
- package/docs/commands/plan.md +1 -1
- package/docs/commands/project.md +1 -1
- package/docs/commands/projects.md +1 -1
- package/docs/commands/providers.md +1 -1
- package/docs/commands/queue.md +1 -1
- package/docs/commands/registry.md +1 -1
- package/docs/commands/reload.md +1 -1
- package/docs/commands/rename.md +1 -1
- package/docs/commands/restore.md +1 -1
- package/docs/commands/resume.md +1 -1
- package/docs/commands/router.md +1 -1
- package/docs/commands/rules.md +70 -0
- package/docs/commands/session.md +1 -1
- package/docs/commands/sessions.md +1 -1
- package/docs/commands/setup.md +1 -1
- package/docs/commands/setups.md +1 -1
- package/docs/commands/shell-init.md +1 -1
- package/docs/commands/skill.md +1 -1
- package/docs/commands/task.md +1 -1
- package/docs/commands/tasks.md +1 -1
- package/docs/commands/topic.md +1 -1
- package/docs/commands/update.md +1 -1
- package/docs/commands/worker.md +1 -1
- package/docs/commands/zettel.md +1 -1
- package/docs/concepts/panes.md +3 -2
- package/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/pai-plugin.json +1 -1
- package/plugins/context-preservation/plugin.json +1 -1
- package/plugins/core/plugin.json +1 -1
- package/plugins/creative/plugin.json +1 -1
- package/plugins/observability/plugin.json +1 -1
- package/plugins/productivity/plugin.json +1 -1
- package/plugins/semantic-search/plugin.json +1 -1
- package/plugins/ui/plugin.json +1 -1
- package/plugins/zettelkasten/plugin.json +1 -1
- package/src/hooks/ts/lib/loop-guard.ts +3 -5
- package/src/hooks/ts/lib/rules/builtin/base64-decode-to-shell.rule.yaml +13 -0
- package/src/hooks/ts/lib/rules/builtin/chmod-world-writable.rule.yaml +13 -0
- package/src/hooks/ts/lib/rules/builtin/chmod-zero.rule.yaml +11 -0
- package/src/hooks/ts/lib/rules/builtin/credential-file-read.rule.yaml +13 -0
- package/src/hooks/ts/lib/rules/builtin/credential-read-then-network.rule.yaml +29 -0
- package/src/hooks/ts/lib/rules/builtin/crontab-persistence.rule.yaml +10 -0
- package/src/hooks/ts/lib/rules/builtin/curl-piped-to-shell.rule.yaml +15 -0
- package/src/hooks/ts/lib/rules/builtin/download-then-execute.rule.yaml +30 -0
- package/src/hooks/ts/lib/rules/builtin/find-root-delete.rule.yaml +11 -0
- package/src/hooks/ts/lib/rules/builtin/git-push-force.rule.yaml +24 -0
- package/src/hooks/ts/lib/rules/builtin/git-push-plus-refspec.rule.yaml +12 -0
- package/src/hooks/ts/lib/rules/builtin/git-reset-hard.rule.yaml +12 -0
- package/src/hooks/ts/lib/rules/builtin/injection-then-tool.rule.yaml +30 -0
- package/src/hooks/ts/lib/rules/builtin/instruction-override-disregard.rule.yaml +12 -0
- package/src/hooks/ts/lib/rules/builtin/instruction-override-ignore.rule.yaml +13 -0
- package/src/hooks/ts/lib/rules/builtin/launchagents-write-persistence.rule.yaml +12 -0
- package/src/hooks/ts/lib/rules/builtin/launchctl-load-persistence.rule.yaml +10 -0
- package/src/hooks/ts/lib/rules/builtin/reverse-shell-dev-tcp.rule.yaml +11 -0
- package/src/hooks/ts/lib/rules/builtin/reverse-shell-interactive-bash.rule.yaml +11 -0
- package/src/hooks/ts/lib/rules/builtin/rm-recursive-parent.rule.yaml +11 -0
- package/src/hooks/ts/lib/rules/builtin/rm-recursive-relative.rule.yaml +11 -0
- package/src/hooks/ts/lib/rules/builtin/rm-recursive-root.rule.yaml +24 -0
- package/src/hooks/ts/lib/rules/builtin/rm-trailing-tilde.rule.yaml +11 -0
- package/src/hooks/ts/lib/rules/builtin/sudoers-nopasswd.rule.yaml +15 -0
- package/src/hooks/ts/lib/rules/builtin.generated.ts +530 -0
- package/src/hooks/ts/lib/rules/eval.ts +74 -0
- package/src/hooks/ts/lib/rules/lint.ts +102 -0
- package/src/hooks/ts/lib/rules/load.ts +229 -0
- package/src/hooks/ts/lib/rules/types.ts +56 -0
- package/src/hooks/ts/pre-tool-use/security-validator.ts +27 -96
- package/src/hooks/ts/user-prompt/handoff-link.ts +34 -0
- package/dist/aibroker-client-BxzeEHPF.mjs +0 -3
- package/dist/auto-route-C3S2W3Bc.mjs +0 -11
- package/dist/budget-BrloTOZW.mjs +0 -15
- package/dist/cockpit-memory-teG9M8bG.mjs +0 -20
- package/dist/config-Cxi-aBf4.mjs +0 -4
- package/dist/daemon-DeYImF1F.mjs +0 -45
- package/dist/detect-Rrimh0xc.mjs +0 -10
- package/dist/factory-Bs9imVMq.mjs +0 -9
- package/dist/kill-tree-SV6hrIa3.mjs +0 -10
- package/dist/operator-BTqvTIc8.mjs +0 -7
- package/dist/providers-FvCLzFge.mjs +0 -16
- package/dist/registrable-BzH4TObg.mjs +0 -9
- package/dist/render-B35bQTE-.mjs +0 -13
- package/dist/render-C84BqQRC.mjs +0 -903
- package/dist/root-DD1xx_Vu.mjs +0 -36
- package/dist/route-table-FEfDuhIV.mjs +0 -15
- package/dist/session-BAhtXdxj.mjs +0 -35
- package/dist/session-scan-B57dNuA4.mjs +0 -9
- package/dist/status-Dm4dYciN.mjs +0 -6
- package/dist/viewer-DEySE4c_.mjs +0 -22
- package/dist/worker-config-CO5FXLAT.mjs +0 -41
- package/dist/workers-config-f8KrkY4L.mjs +0 -4
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lint.ts — run each rule's own fixtures.
|
|
3
|
+
*
|
|
4
|
+
* Every `tests.match` must hit the rule, every `tests.no_match` must not.
|
|
5
|
+
* A single-step rule's fixtures are plain strings tested as Bash commands; a
|
|
6
|
+
* sequence rule's are lists of step contents, checked step by step (the
|
|
7
|
+
* window and ordering are the scan's business, not the fixtures'). A rule
|
|
8
|
+
* with no fixtures is ok (nothing to check).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { evaluateRules } from "./eval.js";
|
|
12
|
+
import type { Rule, RuleSequenceStep } from "./types.js";
|
|
13
|
+
|
|
14
|
+
export interface RuleLint {
|
|
15
|
+
id: string;
|
|
16
|
+
ok: boolean;
|
|
17
|
+
failures: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stepRegex(step: RuleSequenceStep): RegExp | null {
|
|
21
|
+
try {
|
|
22
|
+
return new RegExp(step.match, step.flags ?? "");
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One fixture step: the content tested, optionally the tool it belongs to. */
|
|
29
|
+
interface FixtureStep {
|
|
30
|
+
tool?: string;
|
|
31
|
+
content: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A fixture sequence hits when every step's regex matches its content and its
|
|
36
|
+
* tool pin, when set. A plain string is a Bash-shaped step; an object names
|
|
37
|
+
* the tool, so a rule whose step is pinned to a fetched result (WebFetch,
|
|
38
|
+
* Read) can still carry a fixture that hits. A step whose source
|
|
39
|
+
* backreferences an earlier step's group (`\1`) is checked with that group's
|
|
40
|
+
* capture substituted in — the same resolution the transcript scan applies —
|
|
41
|
+
* so a fixture like download-then-execute's "same file twice" checks the way
|
|
42
|
+
* the scan does.
|
|
43
|
+
*/
|
|
44
|
+
function sequenceHit(rule: Rule, steps: FixtureStep[]): boolean {
|
|
45
|
+
const seqSteps = rule.sequence?.steps ?? [];
|
|
46
|
+
if (steps.length !== seqSteps.length) return false;
|
|
47
|
+
const prior: RegExpMatchArray[] = [];
|
|
48
|
+
for (let i = 0; i < seqSteps.length; i++) {
|
|
49
|
+
const step = seqSteps[i];
|
|
50
|
+
const { tool, content } = steps[i];
|
|
51
|
+
if (step.tool && !new RegExp(step.tool).test(tool ?? "Bash")) return false;
|
|
52
|
+
// a step that names an earlier step's capture (`\1`) is checked with that
|
|
53
|
+
// capture substituted in — the raw `\1` can never match text
|
|
54
|
+
if (step.match.includes("\\1") && prior.length) {
|
|
55
|
+
const cap = prior[0][1];
|
|
56
|
+
if (!cap || !new RegExp(step.match.replace(/\\1/g, cap), step.flags ?? "").test(content)) return false;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const re = stepRegex(step);
|
|
60
|
+
if (!re) return false;
|
|
61
|
+
const m = content.match(re);
|
|
62
|
+
if (!m) return false;
|
|
63
|
+
prior.push(m);
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A fixture sequence as written in yaml: a list of strings or {tool, content} objects. */
|
|
69
|
+
type FixtureSequence = Array<string | { tool?: string; content: string }>;
|
|
70
|
+
|
|
71
|
+
function asFixtureSteps(t: string | FixtureSequence): FixtureStep[] | null {
|
|
72
|
+
if (!Array.isArray(t)) return null;
|
|
73
|
+
return t.map((s) => (typeof s === "string" ? { content: s } : { tool: s.tool, content: s.content }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function lintRules(rules: Rule[]): RuleLint[] {
|
|
77
|
+
return rules.map((rule) => {
|
|
78
|
+
const failures: string[] = [];
|
|
79
|
+
const show = (t: string | FixtureSequence): string =>
|
|
80
|
+
Array.isArray(t) ? t.map((s) => (typeof s === "string" ? s : `${s.tool ?? "Bash"}: ${s.content}`)).join(" | ") : t;
|
|
81
|
+
if (rule.sequence) {
|
|
82
|
+
for (const t of rule.tests?.match ?? []) {
|
|
83
|
+
const steps = asFixtureSteps(t);
|
|
84
|
+
if (!steps || !sequenceHit(rule, steps)) failures.push(`tests.match did not hit: ${show(t)}`);
|
|
85
|
+
}
|
|
86
|
+
for (const t of rule.tests?.no_match ?? []) {
|
|
87
|
+
const steps = asFixtureSteps(t);
|
|
88
|
+
if (steps && sequenceHit(rule, steps)) failures.push(`tests.no_match hit: ${show(t)}`);
|
|
89
|
+
}
|
|
90
|
+
return { id: rule.id, ok: failures.length === 0, failures };
|
|
91
|
+
}
|
|
92
|
+
for (const text of (rule.tests?.match ?? []) as string[]) {
|
|
93
|
+
const hit = evaluateRules([rule], "Bash", text);
|
|
94
|
+
if (!hit || hit.rule.id !== rule.id) failures.push(`tests.match did not hit: ${text}`);
|
|
95
|
+
}
|
|
96
|
+
for (const text of (rule.tests?.no_match ?? []) as string[]) {
|
|
97
|
+
const hit = evaluateRules([rule], "Bash", text);
|
|
98
|
+
if (hit && hit.rule.id === rule.id) failures.push(`tests.no_match hit: ${text}`);
|
|
99
|
+
}
|
|
100
|
+
return { id: rule.id, ok: failures.length === 0, failures };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* load.ts — read `*.rule.yaml` files from directories into rules.
|
|
3
|
+
*
|
|
4
|
+
* One rule per file, or a top-level list of rules. A bad regex or a missing
|
|
5
|
+
* field is reported in `errors` and the rule is skipped — this never throws,
|
|
6
|
+
* because hooks fail open and a broken user rule must not break the hook.
|
|
7
|
+
*
|
|
8
|
+
* Directories are read in order; a later rule with the same id replaces an
|
|
9
|
+
* earlier one (user rules override built-ins), and `severity: off` disables
|
|
10
|
+
* that id entirely.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { parse } from "yaml";
|
|
16
|
+
import { defaultPaiHomeDir, legacyPaiHomeDir } from "../../../../config/pai-home.js";
|
|
17
|
+
import { BUILTIN_RULES } from "./builtin.generated.js";
|
|
18
|
+
import type { LoadedRule, Rule, RuleSequence, RuleSequenceStep, RuleSeverity } from "./types.js";
|
|
19
|
+
|
|
20
|
+
const SEVERITIES: RuleSeverity[] = ["block", "confirm", "warn", "off"];
|
|
21
|
+
|
|
22
|
+
/** A YAML regex node (`/foo/i`) split into source and flags; a plain string is the source. */
|
|
23
|
+
function splitRegex(value: string): { source: string; flags: string } {
|
|
24
|
+
const isRegexNode = value.length > 2 && value.startsWith("/") && /\/[a-z]*$/.test(value);
|
|
25
|
+
return isRegexNode
|
|
26
|
+
? { source: value.slice(1, value.lastIndexOf("/")), flags: value.slice(value.lastIndexOf("/") + 1) }
|
|
27
|
+
: { source: value, flags: "" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A sequence mapping, validated (steps, window, order, every step regex compiles), or null with an error pushed. */
|
|
31
|
+
function asSequence(raw: Record<string, unknown>, id: string, source: string, errors: string[]): RuleSequence | null {
|
|
32
|
+
const seq = raw.sequence;
|
|
33
|
+
if (typeof seq !== "object" || seq === null || Array.isArray(seq)) {
|
|
34
|
+
errors.push(`${source} (${id}): sequence must be a mapping`);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const s = seq as Record<string, unknown>;
|
|
38
|
+
const rawSteps = Array.isArray(s.steps) ? s.steps : [];
|
|
39
|
+
const steps: RuleSequenceStep[] = [];
|
|
40
|
+
for (const item of rawSteps) {
|
|
41
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
42
|
+
errors.push(`${source} (${id}): sequence step must be a mapping`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const step = item as Record<string, unknown>;
|
|
46
|
+
const match = typeof step.match === "string" ? step.match : "";
|
|
47
|
+
if (!match) {
|
|
48
|
+
errors.push(`${source} (${id}): sequence step missing match`);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const split = splitRegex(match);
|
|
52
|
+
const flags = typeof step.flags === "string" ? step.flags : split.flags;
|
|
53
|
+
try {
|
|
54
|
+
new RegExp(split.source, flags);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
errors.push(`${source} (${id}): sequence step invalid regex: ${e instanceof Error ? e.message : String(e)}`);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
steps.push({
|
|
60
|
+
tool: typeof step.tool === "string" ? step.tool : undefined,
|
|
61
|
+
match: split.source,
|
|
62
|
+
flags: flags || undefined,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (!steps.length) {
|
|
66
|
+
errors.push(`${source} (${id}): sequence has no steps`);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const window = Number(s.window_seconds);
|
|
70
|
+
if (!Number.isFinite(window) || window <= 0) {
|
|
71
|
+
errors.push(`${source} (${id}): sequence needs a positive window_seconds`);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const order = s.order === "any" ? "any" : s.order === "strict" ? "strict" : null;
|
|
75
|
+
if (!order) {
|
|
76
|
+
errors.push(`${source} (${id}): sequence order must be strict or any`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return { steps, window_seconds: window, order };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The user rule directories: PAI_HOME/rules and .pai/rules under the cwd.
|
|
84
|
+
* PAI_HOME is resolved like paiHomeDir() but without its stderr migration
|
|
85
|
+
* notice — a hook's stderr is machinery, and reading a rules directory is
|
|
86
|
+
* not a reason to complain about it.
|
|
87
|
+
*/
|
|
88
|
+
export function userRuleDirs(cwd: string = process.cwd()): string[] {
|
|
89
|
+
const home = process.env.PAI_HOME
|
|
90
|
+
? process.env.PAI_HOME
|
|
91
|
+
: !existsSync(join(defaultPaiHomeDir(), "config.yaml")) && existsSync(join(legacyPaiHomeDir(), "config.yaml"))
|
|
92
|
+
? legacyPaiHomeDir()
|
|
93
|
+
: defaultPaiHomeDir();
|
|
94
|
+
return [join(home, "rules"), join(cwd, ".pai", "rules")];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function asRule(value: unknown, source: string, errors: string[]): Rule | null {
|
|
98
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
99
|
+
errors.push(`${source}: not a mapping`);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const raw = value as Record<string, unknown>;
|
|
103
|
+
const id = typeof raw.id === "string" ? raw.id : "";
|
|
104
|
+
const severity = raw.severity as RuleSeverity;
|
|
105
|
+
const reason = typeof raw.reason === "string" ? raw.reason : "";
|
|
106
|
+
if (!id) {
|
|
107
|
+
errors.push(`${source}: missing id`);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (!SEVERITIES.includes(severity)) {
|
|
111
|
+
errors.push(`${source} (${id}): severity must be block, confirm, warn or off`);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
if (!reason) {
|
|
115
|
+
errors.push(`${source} (${id}): missing reason`);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
// `severity: off` disables the id; it needs no match.
|
|
119
|
+
if (severity === "off") return { id, severity, reason };
|
|
120
|
+
const match = typeof raw.match === "string" ? raw.match : "";
|
|
121
|
+
// A sequence rule carries its patterns in `sequence.steps` instead of `match`.
|
|
122
|
+
if (raw.sequence !== undefined) {
|
|
123
|
+
const sequence = asSequence(raw, id, source, errors);
|
|
124
|
+
if (!sequence) return null;
|
|
125
|
+
return { id, severity, sequence, category: typeof raw.category === "string" ? raw.category : undefined, reason, tests: asTests(raw) };
|
|
126
|
+
}
|
|
127
|
+
if (!match) {
|
|
128
|
+
errors.push(`${source} (${id}): missing match`);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
// YAML accepts a bare regex node (`match: /foo/i`); a plain string is the
|
|
132
|
+
// regex source with flags in `flags`.
|
|
133
|
+
const split = splitRegex(match);
|
|
134
|
+
const flags = typeof raw.flags === "string" ? raw.flags : split.flags;
|
|
135
|
+
const source2 = split.source;
|
|
136
|
+
try {
|
|
137
|
+
new RegExp(source2, flags);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
errors.push(`${source} (${id}): invalid regex: ${e instanceof Error ? e.message : String(e)}`);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
id,
|
|
144
|
+
severity,
|
|
145
|
+
tool: typeof raw.tool === "string" ? raw.tool : undefined,
|
|
146
|
+
category: typeof raw.category === "string" ? raw.category : undefined,
|
|
147
|
+
match: source2,
|
|
148
|
+
flags: flags || undefined,
|
|
149
|
+
quoted: raw.quoted === true ? true : undefined,
|
|
150
|
+
reason,
|
|
151
|
+
tests: asTests(raw),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Fixtures: plain strings for a single-step rule, lists of step contents for a sequence rule. */
|
|
156
|
+
function asTests(raw: Record<string, unknown>): Rule["tests"] {
|
|
157
|
+
if (typeof raw.tests !== "object" || raw.tests === null || Array.isArray(raw.tests)) return undefined;
|
|
158
|
+
const t = raw.tests as Record<string, unknown>;
|
|
159
|
+
const list = (v: unknown): string[] | string[][] | undefined => {
|
|
160
|
+
if (!Array.isArray(v)) return undefined;
|
|
161
|
+
return v.every((x) => Array.isArray(x)) ? (v as unknown[][]).map((x) => x.map(String)) : v.map(String);
|
|
162
|
+
};
|
|
163
|
+
return { match: list(t.match), no_match: list(t.no_match) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Rules from one directory's `*.rule.yaml` files, in filename order. */
|
|
167
|
+
function loadDir(dir: string, errors: string[]): LoadedRule[] {
|
|
168
|
+
const out: LoadedRule[] = [];
|
|
169
|
+
let names: string[];
|
|
170
|
+
try {
|
|
171
|
+
names = readdirSync(dir).filter((f) => f.endsWith(".rule.yaml")).sort();
|
|
172
|
+
} catch {
|
|
173
|
+
return out; // missing directory is not an error
|
|
174
|
+
}
|
|
175
|
+
for (const name of names) {
|
|
176
|
+
const source = join(dir, name);
|
|
177
|
+
let text: string;
|
|
178
|
+
try {
|
|
179
|
+
text = readFileSync(source, "utf8");
|
|
180
|
+
} catch (e) {
|
|
181
|
+
errors.push(`${source}: ${e instanceof Error ? e.message : String(e)}`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
let doc: unknown;
|
|
185
|
+
try {
|
|
186
|
+
doc = parse(text);
|
|
187
|
+
} catch (e) {
|
|
188
|
+
errors.push(`${source}: ${e instanceof Error ? e.message : String(e)}`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const items = Array.isArray(doc) ? doc : [doc];
|
|
192
|
+
for (const item of items) {
|
|
193
|
+
const rule = asRule(item, source, errors);
|
|
194
|
+
if (rule) out.push({ ...rule, source });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Built-in rules first, then each directory in order; a later rule with the
|
|
202
|
+
* same id replaces the earlier one, and `severity: off` removes the id.
|
|
203
|
+
*/
|
|
204
|
+
export function loadRules(dirs: string[]): { rules: Rule[]; errors: string[] } {
|
|
205
|
+
const errors: string[] = [];
|
|
206
|
+
const byId = new Map<string, LoadedRule>();
|
|
207
|
+
for (const rule of BUILTIN_RULES) byId.set(rule.id, rule);
|
|
208
|
+
for (const dir of dirs) {
|
|
209
|
+
for (const rule of loadDir(dir, errors)) byId.set(rule.id, rule);
|
|
210
|
+
}
|
|
211
|
+
const rules: Rule[] = [];
|
|
212
|
+
for (const rule of byId.values()) {
|
|
213
|
+
if (rule.severity === "off") continue;
|
|
214
|
+
const { source: _source, ...rest } = rule;
|
|
215
|
+
rules.push(rest);
|
|
216
|
+
}
|
|
217
|
+
return { rules, errors };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Every rule with its source and enabled state, for `pai rules list`. */
|
|
221
|
+
export function listRules(dirs: string[]): { rules: LoadedRule[]; errors: string[] } {
|
|
222
|
+
const errors: string[] = [];
|
|
223
|
+
const byId = new Map<string, LoadedRule>();
|
|
224
|
+
for (const rule of BUILTIN_RULES) byId.set(rule.id, rule);
|
|
225
|
+
for (const dir of dirs) {
|
|
226
|
+
for (const rule of loadDir(dir, errors)) byId.set(rule.id, rule);
|
|
227
|
+
}
|
|
228
|
+
return { rules: [...byId.values()], errors };
|
|
229
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — the shape of a security rule. Rules live in `*.rule.yaml` files
|
|
3
|
+
* (built-ins under ./builtin/, user rules under PAI_HOME/rules and
|
|
4
|
+
* .pai/rules), so patterns can be extended without editing TypeScript, and
|
|
5
|
+
* every rule carries its own fixtures.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type RuleSeverity = "block" | "confirm" | "warn" | "off";
|
|
9
|
+
|
|
10
|
+
/** One step of a sequence rule: a regex on the tool's content, optionally pinned to a tool. */
|
|
11
|
+
export interface RuleSequenceStep {
|
|
12
|
+
/** Regex on the tool name; any tool when absent. */
|
|
13
|
+
tool?: string;
|
|
14
|
+
/** Regex source; flags come from `flags`. */
|
|
15
|
+
match: string;
|
|
16
|
+
flags?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A two-step (or more) pattern: every step must occur within `window_seconds`
|
|
21
|
+
* of the first, in step order when `order` is "strict". Sequence rules are not
|
|
22
|
+
* evaluated live by the hook (eval.ts) — they are for the after-the-fact scan
|
|
23
|
+
* over a transcript (`pai audit scan`).
|
|
24
|
+
*/
|
|
25
|
+
export interface RuleSequence {
|
|
26
|
+
steps: RuleSequenceStep[];
|
|
27
|
+
window_seconds: number;
|
|
28
|
+
order: "strict" | "any";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Rule {
|
|
32
|
+
id: string;
|
|
33
|
+
severity: RuleSeverity;
|
|
34
|
+
/** Regex on the tool name; default "Bash". */
|
|
35
|
+
tool?: string;
|
|
36
|
+
/** Regex source; flags come from `flags`. Required unless severity is "off" or `sequence` is set. */
|
|
37
|
+
match?: string;
|
|
38
|
+
flags?: string;
|
|
39
|
+
/** Multi-step pattern; see RuleSequence. */
|
|
40
|
+
sequence?: RuleSequence;
|
|
41
|
+
/** Also test the text with only the quote marks removed (default: quoted strings are dropped). */
|
|
42
|
+
quoted?: boolean;
|
|
43
|
+
/** Legacy group name shown in deny messages and security events; defaults to the id. */
|
|
44
|
+
category?: string;
|
|
45
|
+
reason: string;
|
|
46
|
+
/** Built-in fixtures: every `match` must hit, every `no_match` must not. For a sequence rule each entry is the list of steps, in step order: a plain string is a Bash-shaped step, an object names the tool a step's content belongs to. */
|
|
47
|
+
tests?: {
|
|
48
|
+
match?: Array<string | string[] | Array<string | { tool?: string; content: string }>>;
|
|
49
|
+
no_match?: Array<string | string[] | Array<string | { tool?: string; content: string }>>;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Where a rule came from, for `pai rules list`. */
|
|
54
|
+
export interface LoadedRule extends Rule {
|
|
55
|
+
source: string;
|
|
56
|
+
}
|
|
@@ -23,75 +23,22 @@ import { dirname, resolve } from 'path';
|
|
|
23
23
|
import { securityEventsPath } from '../lib/pai-paths.js';
|
|
24
24
|
import { isSharedCheckout } from '../lib/shared-checkout.js';
|
|
25
25
|
import { maskSecrets } from '../lib/loop-guard.js';
|
|
26
|
-
import { decideWorkerGitGuard,
|
|
26
|
+
import { decideWorkerGitGuard, workerGitGuardMessage } from '../lib/worker-git-guard.js';
|
|
27
27
|
import { detectUnsafePlutil, plutilGuardMessage } from '../lib/plutil-guard.js';
|
|
28
|
+
import { loadRules, userRuleDirs } from '../lib/rules/load.js';
|
|
29
|
+
import { evaluateRules } from '../lib/rules/eval.js';
|
|
28
30
|
import { readWorkersSection } from '../../../workers/config.js';
|
|
29
31
|
import { workersLogDir } from '../../../workers/paths.js';
|
|
30
32
|
import { worktreesDir } from '../../../workers/worktree.js';
|
|
31
33
|
|
|
32
34
|
// ============================================================================
|
|
33
|
-
// ATTACK
|
|
35
|
+
// ATTACK RULES
|
|
34
36
|
// ============================================================================
|
|
37
|
+
// Patterns live in *.rule.yaml files: built-ins under lib/rules/builtin/
|
|
38
|
+
// (embedded at build time), user rules under PAI_HOME/rules and .pai/rules.
|
|
39
|
+
// Loaded once per hook process; a broken rule file is skipped, never fatal.
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
const REVERSE_SHELL_PATTERNS: RegExp[] = [
|
|
38
|
-
/\/dev\/(tcp|udp)\/[0-9]/, // Bash TCP/UDP device
|
|
39
|
-
/bash\s+-i\s+>&?\s*\/dev\//, // Interactive bash redirect
|
|
40
|
-
// Add your own reverse shell patterns here
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
// Example: Instruction Override (BLOCK - prompt injection)
|
|
44
|
-
const INSTRUCTION_OVERRIDE_PATTERNS: RegExp[] = [
|
|
45
|
-
/ignore\s+(all\s+)?previous\s+instructions?/i,
|
|
46
|
-
/disregard\s+(all\s+)?(prior|previous)\s+(instructions?|rules?)/i,
|
|
47
|
-
// Add your own prompt injection patterns here
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
// Example: Catastrophic Deletion Patterns (BLOCK - filesystem destruction)
|
|
51
|
-
const CATASTROPHIC_DELETION_PATTERNS: RegExp[] = [
|
|
52
|
-
// The destructive verb must sit in command position (line start, after
|
|
53
|
-
// ; & | ( ` $( or sudo), so `~`, `/` and `-` inside awk programs or python
|
|
54
|
-
// strings never match on their own.
|
|
55
|
-
// Recursive rm of /, ~, $HOME or a system directory
|
|
56
|
-
/(?:^|[;&|(`\n]|\$\()\s*(?:sudo\s+(?:-\S+\s+)*)?rm\s+(?:-\S+\s+)*(?:-\w*[rR]\w*|--recursive)\s+(?:-\S+\s+)*(?:\S+\s+)*(?:\/\*?|~\/?\*?|\$\{?HOME\}?\/?\*?|\/(?:usr|etc|bin|sbin|lib|var|opt|home|System|Library|Users|Applications)\/?\*?)(?=\s|;|&|\||$)/m,
|
|
57
|
-
// find / -delete
|
|
58
|
-
/(?:^|[;&|(`\n]|\$\()\s*(?:sudo\s+(?:-\S+\s+)*)?find\s+(?:\/|~|\$\{?HOME\}?)\/?\s+(?:\S+\s+)*?-delete\b/m,
|
|
59
|
-
|
|
60
|
-
// Trailing tilde bypass
|
|
61
|
-
/(?:^|[;&|(`\n]|\$\()\s*(?:sudo\s+(?:-\S+\s+)*)?rm\s+(?:-[rfivd]+\s+)*\S+\s+~\/?(?:\s|$)/m, // rm something ~/
|
|
62
|
-
|
|
63
|
-
// Relative path recursive deletion
|
|
64
|
-
/(?:^|[;&|(`\n]|\$\()\s*(?:sudo\s+(?:-\S+\s+)*)?rm\s+(?:-[rfivd]+\s+)*\.\/\s*$/m, // rm -rf ./
|
|
65
|
-
/(?:^|[;&|(`\n]|\$\()\s*(?:sudo\s+(?:-\S+\s+)*)?rm\s+(?:-[rfivd]+\s+)*\.\.\/\s*$/m, // rm -rf ../
|
|
66
|
-
|
|
67
|
-
// Add your own dangerous deletion patterns here
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
// Example: Dangerous File Operations (BLOCK - data destruction)
|
|
71
|
-
const DANGEROUS_FILE_OPS_PATTERNS: RegExp[] = [
|
|
72
|
-
/\bchmod\s+(-R\s+)?0{3,}/, // chmod 000
|
|
73
|
-
// Add your own dangerous file operation patterns here
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
// OPTIONAL: Operations that require confirmation instead of blocking
|
|
77
|
-
const DANGEROUS_GIT_PATTERNS: RegExp[] = [
|
|
78
|
-
/\bgit\b.*\spush\b.*(\s-f\b|\s--force(\s|$))/i, // git push -f / --force; --force-with-lease and --follow-tags pass
|
|
79
|
-
/\bgit\b.*\spush\b.*\s\+\S/i, // a plus-prefixed refspec forces the update without -f
|
|
80
|
-
/\bgit\s+reset\s+--hard/i, // git reset --hard
|
|
81
|
-
// Add your own git safety patterns here
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
// Combined patterns for fast iteration
|
|
85
|
-
const ALL_BLOCK_PATTERNS: { category: string; patterns: RegExp[] }[] = [
|
|
86
|
-
{ category: 'reverse_shell', patterns: REVERSE_SHELL_PATTERNS },
|
|
87
|
-
{ category: 'instruction_override', patterns: INSTRUCTION_OVERRIDE_PATTERNS },
|
|
88
|
-
{ category: 'catastrophic_deletion', patterns: CATASTROPHIC_DELETION_PATTERNS },
|
|
89
|
-
{ category: 'dangerous_file_ops', patterns: DANGEROUS_FILE_OPS_PATTERNS },
|
|
90
|
-
];
|
|
91
|
-
|
|
92
|
-
const CONFIRM_PATTERNS: { category: string; patterns: RegExp[] }[] = [
|
|
93
|
-
{ category: 'dangerous_git', patterns: DANGEROUS_GIT_PATTERNS },
|
|
94
|
-
];
|
|
41
|
+
const RULES = loadRules(userRuleDirs(process.cwd())).rules;
|
|
95
42
|
|
|
96
43
|
// ============================================================================
|
|
97
44
|
// TYPES
|
|
@@ -147,45 +94,18 @@ function deny(reason: string): never {
|
|
|
147
94
|
interface DetectionResult {
|
|
148
95
|
blocked: boolean;
|
|
149
96
|
requiresConfirmation?: boolean;
|
|
97
|
+
warned?: boolean;
|
|
150
98
|
category?: string;
|
|
151
99
|
pattern?: string;
|
|
152
100
|
}
|
|
153
101
|
|
|
154
|
-
/**
|
|
155
|
-
function dropQuoted(segment: string): string {
|
|
156
|
-
return segment.replace(/"(?:\\.|[^"\\])*"|'[^']*'/g, '""');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** A quoted +refspec is still a forced push, so push segments also test with only the quote marks removed. */
|
|
160
|
-
function gitPatternHit(pattern: RegExp, segment: string): boolean {
|
|
161
|
-
const bare = dropQuoted(segment);
|
|
162
|
-
if (pattern.test(bare)) return true;
|
|
163
|
-
return /\bgit\b.*\spush\b/.test(bare) && pattern.test(segment.replace(/["']/g, ''));
|
|
164
|
-
}
|
|
165
|
-
|
|
102
|
+
/** First block hit, else first confirm hit, else first warn hit (category = the rule's group, else its id). */
|
|
166
103
|
function detectAttack(content: string): DetectionResult {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
if (pattern.test(content) || pattern.test(unquoted)) {
|
|
173
|
-
return { blocked: true, category, pattern: pattern.source };
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Then check for confirmation-required patterns
|
|
179
|
-
const segments = splitSegments(content);
|
|
180
|
-
for (const { category, patterns } of CONFIRM_PATTERNS) {
|
|
181
|
-
for (const pattern of patterns) {
|
|
182
|
-
if (segments.some((seg) => gitPatternHit(pattern, seg))) {
|
|
183
|
-
return { blocked: false, requiresConfirmation: true, category, pattern: pattern.source };
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return { blocked: false };
|
|
104
|
+
const hit = evaluateRules(RULES, 'Bash', content);
|
|
105
|
+
if (!hit) return { blocked: false };
|
|
106
|
+
if (hit.rule.severity === 'block') return { blocked: true, category: hit.rule.category ?? hit.rule.id, pattern: hit.rule.match };
|
|
107
|
+
if (hit.rule.severity === 'confirm') return { blocked: false, requiresConfirmation: true, category: hit.rule.category ?? hit.rule.id, pattern: hit.rule.match };
|
|
108
|
+
return { blocked: false, warned: true, category: hit.rule.category ?? hit.rule.id, pattern: hit.rule.match };
|
|
189
109
|
}
|
|
190
110
|
|
|
191
111
|
// ============================================================================
|
|
@@ -334,7 +254,18 @@ async function main(): Promise<void> {
|
|
|
334
254
|
deny(`This is a dangerous operation (${command.slice(0, 50)}...). This can cause data loss. If you're sure, explicitly confirm this command.`);
|
|
335
255
|
}
|
|
336
256
|
|
|
337
|
-
|
|
257
|
+
if (result.warned) {
|
|
258
|
+
// Log the security event but allow the command.
|
|
259
|
+
logSecurityEvent({
|
|
260
|
+
type: 'security_warning',
|
|
261
|
+
category: result.category,
|
|
262
|
+
pattern: result.pattern,
|
|
263
|
+
command: command.slice(0, 200),
|
|
264
|
+
session_id: input.session_id,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Allow - no further logging, immediate exit
|
|
338
269
|
console.log(JSON.stringify({ permissionDecision: 'allow' }));
|
|
339
270
|
}
|
|
340
271
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* handoff-link.ts — UserPromptSubmit
|
|
5
|
+
*
|
|
6
|
+
* Records a handoff link: when a prompt carries the `[pai-handoff from=<id>]`
|
|
7
|
+
* marker `pai handoff export` tells the user to paste, appends
|
|
8
|
+
* {ts, from, to} to <PAI_HOME>/handoffs/links.jsonl, so the chain of sessions
|
|
9
|
+
* that continued from one brief is reconstructable. Silent on any failure —
|
|
10
|
+
* never blocks a prompt.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { appendHandoffLink } from "../../../session/handoff.js";
|
|
14
|
+
|
|
15
|
+
const MARKER = /\[pai-handoff from=([^\]\s]+)\]/;
|
|
16
|
+
|
|
17
|
+
async function main(): Promise<void> {
|
|
18
|
+
try {
|
|
19
|
+
const chunks: Buffer[] = [];
|
|
20
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
21
|
+
const input = JSON.parse(Buffer.concat(chunks).toString("utf8")) as {
|
|
22
|
+
prompt?: string;
|
|
23
|
+
session_id?: string;
|
|
24
|
+
};
|
|
25
|
+
const prompt = typeof input.prompt === "string" ? input.prompt : "";
|
|
26
|
+
const m = MARKER.exec(prompt);
|
|
27
|
+
if (!m || !input.session_id) return;
|
|
28
|
+
appendHandoffLink(m[1], input.session_id);
|
|
29
|
+
} catch {
|
|
30
|
+
// silent failure — don't block user prompts
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
void main();
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import "./config-XpZWOYOG.mjs";
|
|
2
|
-
import "./config-Bu1_vbY8.mjs";
|
|
3
|
-
import "./paths-CHifubDh.mjs";
|
|
4
|
-
import "./worktree-CRs2gyg0.mjs";
|
|
5
|
-
import "./status-ggJ7lgVg.mjs";
|
|
6
|
-
import "./pool-Dh-q3iJi.mjs";
|
|
7
|
-
import "./registrable-DpyTtajg.mjs";
|
|
8
|
-
import "./pai-marker-BcZyJguM.mjs";
|
|
9
|
-
import { n as formatAutoRouteJson, t as autoRoute } from "./auto-route-21GdHL70.mjs";
|
|
10
|
-
|
|
11
|
-
export { autoRoute, formatAutoRouteJson };
|
package/dist/budget-BrloTOZW.mjs
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import "./config-XpZWOYOG.mjs";
|
|
2
|
-
import "./config-Bu1_vbY8.mjs";
|
|
3
|
-
import "./paths-CHifubDh.mjs";
|
|
4
|
-
import "./status-ggJ7lgVg.mjs";
|
|
5
|
-
import "./scope-D-h6F6DN.mjs";
|
|
6
|
-
import { A as budgetSkipReason, D as budgetExceeded, E as budgetCache, M as budgetWall, N as fetchBudgetReading, O as budgetFresh, P as pickBudgetWindow, T as BudgetCache, j as budgetUntilLabel, k as budgetFull, w as BUDGET_TTL_MS } from "./routing-DmkheCRH.mjs";
|
|
7
|
-
import "./tool-errors-7E13StWd.mjs";
|
|
8
|
-
import "./wait-ByV0OyMZ.mjs";
|
|
9
|
-
import "./local-tiers-BE4jGaMa.mjs";
|
|
10
|
-
import "./caller-identity-yLFb7CKq.mjs";
|
|
11
|
-
import "./capacity-DEveQdFV.mjs";
|
|
12
|
-
import "./local-instances-DOxSgT_L.mjs";
|
|
13
|
-
import "./local-candidate-DLdwQt0s.mjs";
|
|
14
|
-
|
|
15
|
-
export { BUDGET_TTL_MS, BudgetCache, budgetCache, budgetExceeded, budgetFresh, budgetFull, budgetSkipReason, budgetUntilLabel, budgetWall, fetchBudgetReading, pickBudgetWindow };
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import "./config-XpZWOYOG.mjs";
|
|
2
|
-
import "./config-Bu1_vbY8.mjs";
|
|
3
|
-
import "./paths-CHifubDh.mjs";
|
|
4
|
-
import "./status-ggJ7lgVg.mjs";
|
|
5
|
-
import "./scope-D-h6F6DN.mjs";
|
|
6
|
-
import "./routing-DmkheCRH.mjs";
|
|
7
|
-
import "./tool-errors-7E13StWd.mjs";
|
|
8
|
-
import "./wait-ByV0OyMZ.mjs";
|
|
9
|
-
import "./local-tiers-BE4jGaMa.mjs";
|
|
10
|
-
import "./caller-identity-yLFb7CKq.mjs";
|
|
11
|
-
import "./capacity-DEveQdFV.mjs";
|
|
12
|
-
import "./local-instances-DOxSgT_L.mjs";
|
|
13
|
-
import "./local-candidate-DLdwQt0s.mjs";
|
|
14
|
-
import "./providers-C9QC0TtO.mjs";
|
|
15
|
-
import "./main-config-ops-kTXR8Pyv.mjs";
|
|
16
|
-
import { A as cockpitMemoryPayload, F as memoryRunLog, I as rateSeries, L as runProgress, M as latestMemoryJob, N as liveMemoryDeps, P as memoryJobProgress, R as runningMemoryJob, j as fetchDaemonMemory, z as sameModel } from "./cockpit-actions-OodH_vx_.mjs";
|
|
17
|
-
import "./ipc-client-CnYEF31R.mjs";
|
|
18
|
-
import "./embed-pass-e64AwDCa.mjs";
|
|
19
|
-
|
|
20
|
-
export { fetchDaemonMemory };
|
package/dist/config-Cxi-aBf4.mjs
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { A as providerQuotaSkipAt, C as parseMcpSetsValue, D as providerContextWindow, E as parseWorkersConfig, F as writeWorkersSection, M as resolveCapability, N as resolveModelCapability, O as providerCostTier, P as resolveProviderKey, S as parseClassesValue, T as parseProvider, _ as maskKey, a as MODEL_CAPABILITIES, b as nativeOrchestratorModel, c as WorkersConfigError, d as defaultWorkersConfig, f as escalationCloud, g as keysDir, h as isModelCapability, ht as DEFAULT_CONTEXT_WINDOW, i as DEFAULT_POOL_PORT, j as readWorkersSection, k as providerKeyPath, l as assertProviderRunnable, m as isLocalProvider, n as CAPABILITY_NAME_RE, o as PROVIDER_TAGS, p as getProviderOrNative, r as DEFAULT_COST_TIER, s as WORKER_CLASSES, t as ANTHROPIC_NATIVE, u as classModelCapability, v as nativeAnthropicModels, w as parseModelsBlock, x as parseCapabilitiesValue, y as nativeAnthropicProvider } from "./config-XpZWOYOG.mjs";
|
|
2
|
-
import { o as expandHome, s as homeRel } from "./config-Bu1_vbY8.mjs";
|
|
3
|
-
|
|
4
|
-
export { ANTHROPIC_NATIVE, CAPABILITY_NAME_RE, DEFAULT_CONTEXT_WINDOW, DEFAULT_COST_TIER, DEFAULT_POOL_PORT, MODEL_CAPABILITIES, PROVIDER_TAGS, WORKER_CLASSES, WorkersConfigError, assertProviderRunnable, classModelCapability, defaultWorkersConfig, escalationCloud, expandHome, getProviderOrNative, homeRel, isLocalProvider, isModelCapability, keysDir, maskKey, nativeAnthropicModels, nativeAnthropicProvider, nativeOrchestratorModel, parseCapabilitiesValue, parseClassesValue, parseMcpSetsValue, parseModelsBlock, parseProvider, parseWorkersConfig, providerContextWindow, providerCostTier, providerKeyPath, providerQuotaSkipAt, readWorkersSection, resolveCapability, resolveModelCapability, resolveProviderKey, writeWorkersSection };
|