@agent-beam/beam 0.2.20 → 0.2.22

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.
Files changed (67) hide show
  1. package/README.md +19 -0
  2. package/dist/account.js +1 -1
  3. package/dist/account.js.map +1 -1
  4. package/dist/agents.d.ts +8 -2
  5. package/dist/agents.js +7 -7
  6. package/dist/agents.js.map +1 -1
  7. package/dist/cli.js +289 -41
  8. package/dist/cli.js.map +1 -1
  9. package/dist/client.js +23 -6
  10. package/dist/client.js.map +1 -1
  11. package/dist/connect.js +1 -1
  12. package/dist/connect.js.map +1 -1
  13. package/dist/enroll.d.ts +0 -4
  14. package/dist/enroll.js +2 -60
  15. package/dist/enroll.js.map +1 -1
  16. package/dist/forward.d.ts +2 -0
  17. package/dist/forward.js +19 -0
  18. package/dist/forward.js.map +1 -1
  19. package/dist/hook-adapters.js +14 -1
  20. package/dist/hook-adapters.js.map +1 -1
  21. package/dist/install.d.ts +2 -0
  22. package/dist/install.js +58 -6
  23. package/dist/install.js.map +1 -1
  24. package/dist/policy.d.ts +1 -0
  25. package/dist/policy.js +11 -3
  26. package/dist/policy.js.map +1 -1
  27. package/dist/serve.d.ts +2 -0
  28. package/dist/serve.js +47 -2
  29. package/dist/serve.js.map +1 -1
  30. package/dist/setup.js +33 -2
  31. package/dist/setup.js.map +1 -1
  32. package/dist/skill-context.d.ts +11 -0
  33. package/dist/skill-context.js +78 -0
  34. package/dist/skill-context.js.map +1 -0
  35. package/dist/skill-gate.d.ts +21 -0
  36. package/dist/skill-gate.js +180 -0
  37. package/dist/skill-gate.js.map +1 -0
  38. package/dist/skill-inventory.d.ts +81 -0
  39. package/dist/skill-inventory.js +167 -0
  40. package/dist/skill-inventory.js.map +1 -0
  41. package/dist/skill-risk-rules.d.ts +17 -0
  42. package/dist/skill-risk-rules.js +61 -0
  43. package/dist/skill-risk-rules.js.map +1 -0
  44. package/dist/skill-scan.d.ts +27 -0
  45. package/dist/skill-scan.js +157 -0
  46. package/dist/skill-scan.js.map +1 -0
  47. package/dist/skill-setup.d.ts +11 -0
  48. package/dist/skill-setup.js +26 -0
  49. package/dist/skill-setup.js.map +1 -0
  50. package/dist/skill-sync.d.ts +40 -0
  51. package/dist/skill-sync.js +79 -0
  52. package/dist/skill-sync.js.map +1 -0
  53. package/dist/skill-watch.d.ts +16 -0
  54. package/dist/skill-watch.js +41 -0
  55. package/dist/skill-watch.js.map +1 -0
  56. package/dist/skills.d.ts +33 -0
  57. package/dist/skills.js +139 -0
  58. package/dist/skills.js.map +1 -0
  59. package/dist/tamper.d.ts +9 -0
  60. package/dist/tamper.js +116 -0
  61. package/dist/tamper.js.map +1 -0
  62. package/dist/trusted-skills.d.ts +9 -0
  63. package/dist/trusted-skills.js +31 -0
  64. package/dist/trusted-skills.js.map +1 -0
  65. package/dist/tty-prompt.js +3 -1
  66. package/dist/tty-prompt.js.map +1 -1
  67. package/package.json +2 -2
@@ -0,0 +1,157 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, readFile, stat } from "node:fs/promises";
3
+ import { basename, join, relative } from "node:path";
4
+ import { comboFindings, scanRisky } from "./skill-risk-rules.js";
5
+ import { firstInstruction } from "./skill-context.js";
6
+ import { detect, redact, risk, scanText, severityRank } from "./core.js";
7
+ const ruleMatches = (rule, text) => rule.re ? new RegExp(rule.re.source, rule.re.flags.replace("g", "")).test(text) : Boolean(rule.match?.(text));
8
+ const injectionRules = [
9
+ { id: "injection.role_override", title: "Role or system-prompt override", severity: "critical", explanation: "The skill tells the agent to drop its existing instructions or adopt a new identity.", re: /\b(?:ignore|disregard)\s+(?:all\s+|any\s+)?(?:of\s+)?(?:the\s+|your\s+)?(?:previous|prior|above|earlier|preceding)\s+(?:system\s+)?(?:instructions|prompts?|rules|messages)\b|\b(?:disregard|forget|override|bypass)\s+(?:all\s+|any\s+)?(?:your|the|previous|prior|above)\s+(?:system\s+)?(?:prompt|instructions|rules|guidelines|safeguards)\b|\bforget\s+everything\s+(?:above|before)\b|\byou\s+are\s+now\s+(?:in\s+)?(?:DAN|developer\s+mode|an?\s+unrestricted)\b/i },
10
+ { id: "injection.fake_delimiter", title: "Forged conversation or system delimiter", severity: "critical", explanation: "Chat-template or 'end of skill' markers are used to make the following text look like a higher-trust message.", re: /<\|(?:im_start|im_end|system|endoftext)\|>|<\/?system(?:-reminder)?>|\[\/?INST\]|---\s*END\s+(?:OF\s+)?SKILL\s*---|\bBEGIN\s+SYSTEM\s+PROMPT\b/i },
11
+ { id: "injection.concealment", title: "Instruction to hide activity from the user", severity: "high", explanation: "The skill asks the agent to act without telling the user or to hide what it did.", re: /(?:without|don'?t|do\s+not|never)\s+(?:telling|tell|mention(?:ing)?|inform(?:ing)?|reveal(?:ing)?|show(?:ing)?)\s+(?:the\s+)?(?:user|human|operator)\b(?!\s+to\b)|\b(?:silently|secretly|covertly)\s+(?:run|execute|send|upload|post|fetch|install|delete|modify)\b|hide\s+(?:this|these|the)\s+(?:step|action|output|command)s?\s+from/i },
12
+ { id: "injection.priority_hijack", title: "Attempt to outrank the user or system", severity: "high", explanation: "The skill claims its instructions take precedence over the user's or the platform's.", re: /\b(?:these|this)\s+instructions?\s+(?:take|has|have)\s+(?:priority|precedence)\s+over|\bbefore\s+(?:doing\s+)?anything\s+else,?\s+(?:you\s+must\s+)?(?:(?:fetch|send|curl|wget|download|upload)\b|(?:run|execute)\s+[`"']?(?:curl|wget|nc|ncat|bash|sh|eval|powershell)\b)/i },
13
+ { id: "injection.hidden_text", context: false, title: "Hidden or invisible text", severity: "high", explanation: "Zero-width, bidirectional-control, or Unicode tag characters can smuggle instructions a human reviewer never sees.", re: /[​-‏‪-‮⁠-⁤]|[\u{E0000}-\u{E007F}]/u },
14
+ { id: "injection.hidden_comment", title: "Instructions inside an HTML comment", severity: "high", explanation: "HTML comments are invisible when a skill is rendered but are still read by the agent.", re: /(?:ignore\s+(?:all\s+|the\s+)?(?:previous|prior|above)|\byou\s+(?:must|should|will)\b|(?:new|hidden|secret)\s+instructions?|system\s+prompt|do\s+not\s+(?:tell|mention|reveal)|\b(?:curl|wget)\s+\S+|\bsend\s+(?:it|this|the\s+\w+)\s+to\b|exfiltrat\w+|\bassistant\s*:)/i },
15
+ { id: "injection.encoded_payload", title: "Encoded payload with a follow-instruction", severity: "high", explanation: "A base64 or hex blob is paired with a request to decode and act on it.", match: s => /(?:[A-Za-z0-9+/]{80,}={0,2})/.test(s) && /\b(?:decode|decrypt|deobfuscate)\b[^\n]{0,60}\b(?:and\s+)?(?:follow|run|execute|obey|apply)\b/i.test(s) },
16
+ { id: "injection.exfil_instruction", title: "Instruction to send data to an external address", severity: "critical", explanation: "The skill directs the agent to transmit conversation, files, or environment to a remote URL.", re: /\b(?:send|post|upload|forward|include|append)\b[^\n]{0,80}\b(?:conversation|chat\s+history|system\s+prompt|context|transcript|environment|env\s+vars?|credentials?|api\s+keys?|source\s+code)\b[^\n]{0,80}https?:\/\//i },
17
+ ];
18
+ // 0-100. Prompt injection is weighted up because a skill is text the agent will obey, and one
19
+ // critical finding alone is already enough to land in HIGH.
20
+ const WEIGHT = { critical: 40, high: 20, medium: 8, info: 2 };
21
+ export function skillRiskScore(findings) {
22
+ // Each rule counts once per skill (a big skill repeating "production" across 30 files is one
23
+ // signal, not thirty). Ordinary rules that merely *mention* env vars, sudo or curl are common in
24
+ // legitimate skills, so their non-critical total is capped below the HIGH band; reaching HIGH or
25
+ // CRITICAL takes prompt injection or a genuinely critical pattern.
26
+ const unique = [...new Map(findings.map(f => [f.id, f])).values()];
27
+ const injection = unique.filter(f => f.id.startsWith("injection.")).reduce((s, f) => s + WEIGHT[f.severity] * 1.5, 0);
28
+ const critical = unique.filter(f => !f.id.startsWith("injection.") && f.severity === "critical").length * WEIGHT.critical;
29
+ const ordinary = Math.min(35, unique.filter(f => !f.id.startsWith("injection.") && f.severity !== "critical").reduce((s, f) => s + WEIGHT[f.severity] / (f.id.startsWith("risk.") ? 1 : 2), 0));
30
+ const score = Math.min(100, Math.round(injection + critical + ordinary));
31
+ return { score, level: score >= 70 ? "CRITICAL" : score >= 40 ? "HIGH" : score >= 15 ? "MEDIUM" : "LOW" };
32
+ }
33
+ // Core rules that don't fit skill prose. `instructions.override` is the older, context-blind version
34
+ // of injection.role_override / injection.concealment, which already cover it with quoting and
35
+ // negation handled. `mcp.write` matches tool identifiers such as mcp__memory__memory_write, which
36
+ // in live events means "the agent is calling a write tool", but in a skill is a documentation
37
+ // mention of a tool by name.
38
+ const SKIP_CORE_FOR_SKILLS = new Set(["instructions.override", "mcp.write"]);
39
+ const TEXT_EXT = /\.(?:md|mdx|txt|json|ya?ml|toml|sh|bash|zsh|py|js|mjs|cjs|ts|rb|ps1)$/i;
40
+ const SKIP_DIR = new Set([".git", "node_modules", ".venv", "__pycache__"]);
41
+ const MAX_FILES = 200;
42
+ const MAX_FILE_BYTES = 500_000;
43
+ async function listFiles(root, dir = root, out = []) {
44
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
45
+ if (out.length >= MAX_FILES)
46
+ break;
47
+ if (entry.isSymbolicLink() || SKIP_DIR.has(entry.name))
48
+ continue;
49
+ const full = join(dir, entry.name);
50
+ if (entry.isDirectory())
51
+ await listFiles(root, full, out);
52
+ else if (entry.isFile())
53
+ out.push(full);
54
+ }
55
+ return out;
56
+ }
57
+ // A finding's identity for review purposes: the rule, the file inside the skill, and the text of
58
+ // the line -- but not the line number, so a "safe" verdict survives the line moving when the skill
59
+ // is edited elsewhere, and stops applying the moment the flagged text itself changes. Computed on
60
+ // the device (where the full text is) and never reversible to the line.
61
+ export function findingFingerprint(f, file) {
62
+ const text = redact(f.evidence ?? "").replace(/\s+/g, " ").trim().toLowerCase().slice(0, 300);
63
+ return createHash("sha256").update(`${f.id}|${file}|${text}`).digest("hex").slice(0, 16);
64
+ }
65
+ const lineOf = (content, index) => content.slice(0, index).split("\n").length;
66
+ const snippet = (text) => redact(text.trim().replace(/\s+/g, " ")).slice(0, 200);
67
+ export function scanInjection(content) {
68
+ const findings = [];
69
+ const lines = content.split(/\r?\n/);
70
+ const add = (rule, evidence, line) => findings.push({ id: rule.id, title: rule.title, severity: rule.severity, explanation: rule.explanation, evidence, line });
71
+ for (const rule of injectionRules) {
72
+ // These two span lines, so they are matched against the whole file -- but each still points at
73
+ // the line and text that triggered it, since that is what a reviewer needs to see.
74
+ if (rule.id === "injection.hidden_comment") {
75
+ for (const block of content.matchAll(/<!--[\s\S]*?-->/g)) {
76
+ if (ruleMatches(rule, block[0])) {
77
+ add(rule, snippet(block[0]), lineOf(content, block.index ?? 0));
78
+ break;
79
+ }
80
+ }
81
+ continue;
82
+ }
83
+ if (rule.id === "injection.encoded_payload") {
84
+ if (!ruleMatches(rule, content))
85
+ continue;
86
+ const at = lines.findIndex(l => /\b(?:decode|decrypt|deobfuscate)\b[^\n]{0,60}\b(?:and\s+)?(?:follow|run|execute|obey|apply)\b/i.test(l));
87
+ add(rule, at >= 0 ? snippet(lines[at]) : "Long encoded string with a decode-and-run instruction", at >= 0 ? at + 1 : 1);
88
+ continue;
89
+ }
90
+ for (let i = 0; i < lines.length; i++) {
91
+ // A phrase that is only being quoted, refused, or given as an example is not an instruction.
92
+ const hit = rule.re && rule.context !== false ? firstInstruction(lines[i], rule.re, "phrase", { prev: lines[i - 1], next: lines[i + 1] }) : ruleMatches(rule, lines[i]) ? { index: 0 } : null;
93
+ if (hit) {
94
+ add(rule, snippet(lines[i]), i + 1);
95
+ break;
96
+ }
97
+ }
98
+ }
99
+ return findings;
100
+ }
101
+ // scanText looks at three-line windows (so a credential on one line and a "send it to" on the next
102
+ // are seen together) and reports the window's first line. For a reviewer that is misleading: the
103
+ // risky text can be two lines further down. When the rule matches a single line on its own, point
104
+ // at that line and show only it; when it needs the lines together, keep the window.
105
+ function narrowToLine(finding, content) {
106
+ if (!finding.line)
107
+ return finding;
108
+ const lines = content.split(/\r?\n/);
109
+ for (let k = 0; k < 3; k++) {
110
+ const text = lines[finding.line - 1 + k];
111
+ if (text !== undefined && detect(text).some(f => f.id === finding.id))
112
+ return { ...finding, line: finding.line + k, evidence: snippet(text) };
113
+ }
114
+ return { ...finding, evidence: snippet(finding.evidence) };
115
+ }
116
+ // Scans every text file in a skill folder -- not just SKILL.md, since bundled scripts and
117
+ // reference docs are loaded or executed by the agent too. Never executes anything.
118
+ export async function scanSkillDir(dir, name) {
119
+ return scanPaths(dir, name, await listFiles(dir));
120
+ }
121
+ // One markdown file on its own (a stray skills.md), without sweeping its whole parent folder.
122
+ export async function scanSkillFile(file) {
123
+ return scanPaths(join(file, ".."), basename(file), [file]);
124
+ }
125
+ async function scanPaths(dir, name, paths) {
126
+ const skipped = [];
127
+ const files = [];
128
+ const hash = createHash("sha256");
129
+ for (const path of paths) {
130
+ const rel = relative(dir, path);
131
+ if (!TEXT_EXT.test(path))
132
+ continue;
133
+ if ((await stat(path)).size > MAX_FILE_BYTES) {
134
+ skipped.push(`${rel} (over 500 KB)`);
135
+ continue;
136
+ }
137
+ const content = await readFile(path, "utf8");
138
+ if (!content.trim())
139
+ continue;
140
+ hash.update(`${rel}\0${content}\0`);
141
+ const findings = [...scanInjection(content), ...scanRisky(content)];
142
+ try {
143
+ for (const f of scanText(rel, content, "skill").findings)
144
+ if (!SKIP_CORE_FOR_SKILLS.has(f.id) && !findings.some(x => x.id === f.id))
145
+ findings.push(narrowToLine(f, content));
146
+ }
147
+ catch { /* over the scanText size cap; injection rules above still ran */ }
148
+ files.push({ file: rel, findings });
149
+ }
150
+ const fileFindings = files.flatMap(f => f.findings.map(x => ({ ...x, file: f.file })));
151
+ // Combinations (e.g. sudo in one file, a broad rm in another) are judged over the whole skill or per file, see skill-risk-rules.ts.
152
+ const combos = comboFindings(files.map(f => ({ file: f.file, ids: new Set(f.findings.map(x => x.id)) })));
153
+ const findings = [...fileFindings, ...combos]
154
+ .sort((a, b) => severityRank[b.severity] - severityRank[a.severity]);
155
+ return { name, dir, hash: hash.digest("hex"), ...skillRiskScore(findings), risk: risk(findings), files, findings, filesScanned: files.length, skipped };
156
+ }
157
+ //# sourceMappingURL=skill-scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-scan.js","sourceRoot":"","sources":["../src/skill-scan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import { type SkillSyncResult } from "./skill-sync.js";
2
+ export type SkillSetupResult = {
3
+ total: number;
4
+ highRisk: number;
5
+ searchedHome: boolean;
6
+ sync: SkillSyncResult;
7
+ };
8
+ export declare function indexAndReportSkills(options: {
9
+ searchProjects: boolean;
10
+ home?: string;
11
+ }): Promise<SkillSetupResult>;
@@ -0,0 +1,26 @@
1
+ import { agentSkillRoots, indexSkills, readInventory } from "./skill-inventory.js";
2
+ import { syncSkillInventory } from "./skill-sync.js";
3
+ // What `beam setup` does about skills: index what is installed and, if this device is connected
4
+ // to a workspace, report the names and risk scores right away instead of waiting for the daily
5
+ // check. The agents' own skill folders are always covered (no macOS folder prompt). Searching the
6
+ // rest of the home folder for skills inside project repos is what raises the "beam would like to
7
+ // access Desktop / Documents / Downloads" prompt, so it only happens when the caller asked for it
8
+ // (setup asks; a non-interactive run does not). Doing it once also switches on the daily
9
+ // whole-home rescan in skill-watch.ts.
10
+ export async function indexAndReportSkills(options) {
11
+ const roots = await agentSkillRoots(options.home);
12
+ if (options.searchProjects)
13
+ await indexSkills({}, options.home);
14
+ else if (roots.length)
15
+ await indexSkills({ roots }, options.home);
16
+ const inventory = await readInventory();
17
+ const skills = inventory?.skills ?? [];
18
+ const sync = await syncSkillInventory({ home: options.home }).catch(() => ({ status: "failed", reason: "unexpected error" }));
19
+ return {
20
+ total: skills.length,
21
+ highRisk: skills.filter(s => s.level === "HIGH" || s.level === "CRITICAL").length,
22
+ searchedHome: options.searchProjects,
23
+ sync,
24
+ };
25
+ }
26
+ //# sourceMappingURL=skill-setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-setup.js","sourceRoot":"","sources":["../src/skill-setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAE,kBAAkB,EAAwB,MAAM,iBAAiB,CAAC;AAI3E,gGAAgG;AAChG,+FAA+F;AAC/F,kGAAkG;AAClG,iGAAiG;AACjG,kGAAkG;AAClG,yFAAyF;AACzF,uCAAuC;AACvC,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,OAAmD;IAC5F,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,IAAI,OAAO,CAAC,cAAc;QAAE,MAAM,WAAW,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;SAC3D,IAAI,KAAK,CAAC,MAAM;QAAE,MAAM,WAAW,CAAC,EAAE,KAAK,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,MAAM,SAAS,GAAG,MAAM,aAAa,EAAE,CAAC;IACxC,MAAM,MAAM,GAAG,SAAS,EAAE,MAAM,IAAI,EAAE,CAAC;IACvC,MAAM,IAAI,GAAG,MAAM,kBAAkB,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAoB,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAAC,CAAC;IAC/I,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,MAAM;QACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,IAAI,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,MAAM;QACjF,YAAY,EAAE,OAAO,CAAC,cAAc;QACpC,IAAI;KACL,CAAC;AACJ,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { type InventorySkill } from "./skill-inventory.js";
2
+ export type SkillReportItem = {
3
+ id: string;
4
+ name: string;
5
+ agent: string;
6
+ scope: string;
7
+ path: string;
8
+ repo: string | null;
9
+ hash: string;
10
+ score: number;
11
+ level: string;
12
+ findings: number;
13
+ injection: boolean;
14
+ trusted: boolean;
15
+ topFindings: NonNullable<InventorySkill["topFindings"]>;
16
+ allFindings: NonNullable<InventorySkill["allFindings"]>;
17
+ files: number;
18
+ firstSeen: string;
19
+ lastSeen: string;
20
+ modifiedAt: string | null;
21
+ };
22
+ export type SkillReport = {
23
+ generatedAt: string;
24
+ day: string;
25
+ skills: SkillReportItem[];
26
+ };
27
+ export declare const MAX_REPORT_SKILLS = 1000;
28
+ export declare function sendMatchedLines(env?: NodeJS.ProcessEnv): boolean;
29
+ export declare function redactPath(dir: string, home?: string): string;
30
+ export declare function buildSkillReport(home?: string): Promise<SkillReport | null>;
31
+ export declare function fingerprint(report: SkillReport): string;
32
+ export type SkillSyncResult = {
33
+ status: "sent" | "unchanged" | "not-enrolled" | "no-inventory" | "failed";
34
+ skills?: number;
35
+ reason?: string;
36
+ };
37
+ export declare function syncSkillInventory(options?: {
38
+ force?: boolean;
39
+ home?: string;
40
+ }): Promise<SkillSyncResult>;
@@ -0,0 +1,79 @@
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { basename, join } from "node:path";
4
+ import { getDataDirectory } from "./config.js";
5
+ import { readIdentity } from "./enroll.js";
6
+ import { forwardSkillInventory } from "./forward.js";
7
+ import { readInventory, trustedHashes } from "./skill-inventory.js";
8
+ import { createHash } from "node:crypto";
9
+ export const MAX_REPORT_SKILLS = 1000;
10
+ // The matched line of each finding is sent so the dashboard can show what exactly triggered it.
11
+ // That is skill text leaving the machine (secrets are redacted first, and it is capped at a few
12
+ // lines per skill), so it can be switched off: BEAM_SKILL_SEND_LINES=0 sends titles, locations
13
+ // and explanations only.
14
+ export function sendMatchedLines(env = process.env) {
15
+ return !["0", "false", "off", "no"].includes(String(env.BEAM_SKILL_SEND_LINES ?? "").trim().toLowerCase());
16
+ }
17
+ const stateFile = () => join(getDataDirectory(), "skills-sync.json");
18
+ const today = () => new Date().toISOString().slice(0, 10);
19
+ export function redactPath(dir, home = homedir()) {
20
+ return dir === home ? "~" : dir.startsWith(home + "/") || dir.startsWith(home + "\\") ? `~${dir.slice(home.length)}` : dir;
21
+ }
22
+ export async function buildSkillReport(home = homedir()) {
23
+ const inventory = await readInventory();
24
+ if (!inventory)
25
+ return null;
26
+ const trusted = await trustedHashes();
27
+ const lines = sendMatchedLines();
28
+ // Riskiest first, so the cap (a safety limit) only ever drops the least interesting skills.
29
+ const skills = inventory.skills.slice(0, MAX_REPORT_SKILLS).map((s) => ({
30
+ id: s.id ?? createHash("sha256").update(s.dir).digest("hex").slice(0, 16), name: s.name, agent: s.agent, scope: s.scope,
31
+ path: redactPath(s.dir, home), repo: s.repo ? basename(s.repo) : null, hash: s.hash, score: s.score, level: s.level,
32
+ findings: s.findings, injection: s.injection, trusted: trusted.has(s.hash), allFindings: s.allFindings ?? [], topFindings: (s.topFindings ?? []).map(({ evidence, ...rest }) => (lines && evidence ? { ...rest, evidence } : rest)),
33
+ files: s.files, firstSeen: s.firstSeen, lastSeen: s.lastSeen, modifiedAt: s.modifiedAt,
34
+ }));
35
+ return { generatedAt: inventory.generatedAt, day: today(), skills };
36
+ }
37
+ // What the workspace already knows. Dates are left out on purpose: re-indexing bumps lastSeen on
38
+ // every skill, and that alone must never count as an update. A skill added, removed, edited (new
39
+ // hash), re-scored, or trusted/untrusted does change it.
40
+ export function fingerprint(report) {
41
+ // The findings are hashed in too, so a report that gains matched lines (an upgrade, or the switch
42
+ // above being flipped) is sent once even though no skill's score changed.
43
+ const detail = (s) => createHash("sha256").update(JSON.stringify([s.topFindings, s.allFindings])).digest("hex").slice(0, 12);
44
+ return createHash("sha256").update(report.skills.map(s => `${s.id}|${s.name}|${s.score}|${s.level}|${s.hash}|${s.trusted}|${detail(s)}`).sort().join("\n")).digest("hex");
45
+ }
46
+ async function lastSent() {
47
+ try {
48
+ const state = JSON.parse(await readFile(stateFile(), "utf8"));
49
+ return { fingerprint: typeof state.fingerprint === "string" ? state.fingerprint : null };
50
+ }
51
+ catch {
52
+ return { fingerprint: null };
53
+ }
54
+ }
55
+ async function recordSent(print, day) {
56
+ await mkdir(getDataDirectory(), { recursive: true, mode: 0o700 });
57
+ const temp = `${stateFile()}.tmp`;
58
+ await writeFile(temp, JSON.stringify({ fingerprint: print, day, sentAt: new Date().toISOString() }) + "\n", { mode: 0o600 });
59
+ await rename(temp, stateFile());
60
+ }
61
+ // Sends the skill names and risk scores to the workspace collector only when they differ from what
62
+ // was last sent -- a new skill, a changed one, or a removed one. An unchanged machine sends nothing,
63
+ // however often this runs. A failed send isn't recorded, so the next run retries it.
64
+ export async function syncSkillInventory(options = {}) {
65
+ if (!await readIdentity())
66
+ return { status: "not-enrolled" };
67
+ const report = await buildSkillReport(options.home);
68
+ if (!report)
69
+ return { status: "no-inventory" };
70
+ const print = fingerprint(report);
71
+ if (!options.force && (await lastSent()).fingerprint === print)
72
+ return { status: "unchanged" };
73
+ const sent = await forwardSkillInventory(report);
74
+ if (!sent.ok)
75
+ return { status: "failed", reason: sent.reason };
76
+ await recordSent(print, report.day);
77
+ return { status: "sent", skills: report.skills.length };
78
+ }
79
+ //# sourceMappingURL=skill-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-sync.js","sourceRoot":"","sources":["../src/skill-sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { type IndexChanges } from "./skill-inventory.js";
2
+ import { type SkillSyncResult } from "./skill-sync.js";
3
+ export declare const CHECK_EVERY_MS: number;
4
+ export type SkillCheck = {
5
+ scanned: "agents" | "full" | "none";
6
+ changes: IndexChanges;
7
+ sync: SkillSyncResult | null;
8
+ };
9
+ export declare function skillCheckDue(now?: number): Promise<boolean>;
10
+ export declare function checkSkills(options?: {
11
+ home?: string;
12
+ }): Promise<SkillCheck>;
13
+ export declare function checkSkillsIfDue(options?: {
14
+ home?: string;
15
+ now?: number;
16
+ }): Promise<SkillCheck | null>;
@@ -0,0 +1,41 @@
1
+ import { agentSkillRoots, indexSkills, readInventory } from "./skill-inventory.js";
2
+ import { syncSkillInventory } from "./skill-sync.js";
3
+ export const CHECK_EVERY_MS = 24 * 60 * 60_000;
4
+ // Due when there is no inventory yet, or the last index (this watch, or a manual `beam skills
5
+ // index` / `beam skills sync`) is a day old. Reading one small file is all this costs.
6
+ export async function skillCheckDue(now = Date.now()) {
7
+ const inventory = await readInventory();
8
+ const last = inventory ? Date.parse(inventory.generatedAt) : NaN;
9
+ return !Number.isFinite(last) || now - last >= CHECK_EVERY_MS;
10
+ }
11
+ // The daily skill scan:
12
+ // - if you have already searched your whole home folder (`beam skills index` or `beam skills
13
+ // list`), repeat that search, so skills added in project folders are found too;
14
+ // - otherwise only check the agents' own skill folders. It never starts the whole-home search on
15
+ // its own: that search is what makes macOS ask for access to Desktop, Documents and
16
+ // Downloads, and it should only happen when you asked for it;
17
+ // - then report to the workspace if anything changed (skill-sync.ts sends nothing otherwise).
18
+ export async function checkSkills(options = {}) {
19
+ const previous = await readInventory();
20
+ let scanned = "none";
21
+ let changes = { added: [], modified: [], removed: [] };
22
+ if (previous?.lastFullScanAt) {
23
+ ({ changes } = await indexSkills({}, options.home));
24
+ scanned = "full";
25
+ }
26
+ else {
27
+ const roots = await agentSkillRoots(options.home);
28
+ if (roots.length) {
29
+ ({ changes } = await indexSkills({ roots }, options.home));
30
+ scanned = "agents";
31
+ }
32
+ }
33
+ if (scanned === "none" && !previous)
34
+ return { scanned, changes, sync: null };
35
+ return { scanned, changes, sync: await syncSkillInventory({ home: options.home }).catch(() => null) };
36
+ }
37
+ // What the background service calls: does nothing unless a day has passed.
38
+ export async function checkSkillsIfDue(options = {}) {
39
+ return await skillCheckDue(options.now) ? checkSkills(options) : null;
40
+ }
41
+ //# sourceMappingURL=skill-watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-watch.js","sourceRoot":"","sources":["../src/skill-watch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAqB,MAAM,sBAAsB,CAAC;AACtG,OAAO,EAAE,kBAAkB,EAAwB,MAAM,iBAAiB,CAAC;AAE3E,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC;AAI/C,8FAA8F;AAC9F,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IAClD,MAAM,SAAS,GAAG,MAAM,aAAa,EAAE,CAAC;IACxC,MAAM,IAAI,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACjE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,GAAG,GAAG,IAAI,IAAI,cAAc,CAAC;AAChE,CAAC;AAED,wBAAwB;AACxB,8FAA8F;AAC9F,mFAAmF;AACnF,kGAAkG;AAClG,uFAAuF;AACvF,iEAAiE;AACjE,+FAA+F;AAC/F,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,UAA6B,EAAE;IAC/D,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC;IACvC,IAAI,OAAO,GAA0B,MAAM,CAAC;IAC5C,IAAI,OAAO,GAAiB,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IACrE,IAAI,QAAQ,EAAE,cAAc,EAAE,CAAC;QAC7B,CAAC,EAAE,OAAO,EAAE,GAAG,MAAM,WAAW,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;QACpD,OAAO,GAAG,MAAM,CAAC;IACnB,CAAC;SAAM,CAAC;QACN,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAClD,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YAAC,CAAC,EAAE,OAAO,EAAE,GAAG,MAAM,WAAW,CAAC,EAAE,KAAK,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;YAAC,OAAO,GAAG,QAAQ,CAAC;QAAC,CAAC;IACvG,CAAC;IACD,IAAI,OAAO,KAAK,MAAM,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC7E,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;AACxG,CAAC;AAED,2EAA2E;AAC3E,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,UAA2C,EAAE;IAClF,OAAO,MAAM,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACxE,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { type SkillScan } from "./skill-scan.js";
2
+ export declare const SKILL_TARGETS: Record<string, {
3
+ project: string;
4
+ global: string;
5
+ }>;
6
+ export declare const ALL_SKILL_AGENTS: string[];
7
+ export declare const SKILL_DIRS: string[];
8
+ export type AddSkillOptions = {
9
+ skill?: string;
10
+ agent?: string[];
11
+ allAgents?: boolean;
12
+ global?: boolean;
13
+ force?: boolean;
14
+ scanOnly?: boolean;
15
+ cwd?: string;
16
+ home?: string;
17
+ };
18
+ export type AddSkillResult = {
19
+ source: string;
20
+ scans: SkillScan[];
21
+ installed: Array<{
22
+ name: string;
23
+ path: string;
24
+ }>;
25
+ blocked: string[];
26
+ };
27
+ export declare const isBlocked: (scan: SkillScan) => boolean;
28
+ export declare function discoverSkills(root: string): Promise<Array<{
29
+ name: string;
30
+ dir: string;
31
+ }>>;
32
+ export declare function addSkill(source: string, options?: AddSkillOptions): Promise<AddSkillResult>;
33
+ export declare function scanInstalledSkills(home?: string, cwd?: string): Promise<SkillScan[]>;
package/dist/skills.js ADDED
@@ -0,0 +1,139 @@
1
+ import { execFile } from "node:child_process";
2
+ import { cp, lstat, mkdir, mkdtemp, readdir, rm, stat } from "node:fs/promises";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { basename, join, resolve } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { severityRank } from "./core.js";
7
+ import { scanSkillDir } from "./skill-scan.js";
8
+ const run = promisify(execFile);
9
+ // Skill directory per agent, relative to the project (or home with --global). Codex, Cursor and
10
+ // Gemini CLI and OpenCode share the universal .agents/skills location. Copilot CLI is the one
11
+ // agent whose project and user folders differ (.github/skills vs ~/.copilot/skills).
12
+ export const SKILL_TARGETS = {
13
+ "claude-code": { project: ".claude/skills", global: ".claude/skills" },
14
+ codex: { project: ".agents/skills", global: ".agents/skills" },
15
+ cursor: { project: ".agents/skills", global: ".agents/skills" },
16
+ gemini: { project: ".agents/skills", global: ".agents/skills" },
17
+ opencode: { project: ".agents/skills", global: ".agents/skills" },
18
+ "copilot-cli": { project: ".github/skills", global: ".copilot/skills" },
19
+ };
20
+ export const ALL_SKILL_AGENTS = Object.keys(SKILL_TARGETS);
21
+ // Every folder skills can live in, for scanning what's already installed.
22
+ export const SKILL_DIRS = [...new Set(Object.values(SKILL_TARGETS).flatMap(t => [t.project, t.global]))];
23
+ // Blocks on prompt-injection findings at high+, or any critical finding. Other high findings
24
+ // (production changes, metadata endpoints...) only warn: legitimate skills, defensive security
25
+ // ones especially, describe those things without being malicious.
26
+ export const isBlocked = (scan) => scan.findings.some(f => f.severity === "critical" || (f.id.startsWith("injection.") && severityRank[f.severity] >= severityRank.high));
27
+ const SHORTHAND = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/;
28
+ const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
29
+ async function isDir(path) { try {
30
+ return (await stat(path)).isDirectory();
31
+ }
32
+ catch {
33
+ return false;
34
+ } }
35
+ async function hasSkillMd(dir) { try {
36
+ return (await stat(join(dir, "SKILL.md"))).isFile();
37
+ }
38
+ catch {
39
+ return false;
40
+ } }
41
+ function cloneUrl(source) {
42
+ const short = source.match(SHORTHAND);
43
+ if (short)
44
+ return `https://github.com/${short[1]}/${short[2].replace(/\.git$/, "")}.git`;
45
+ if (/^https:\/\/(?:github\.com|gitlab\.com)\/[\w.-]+\/[\w.-]+?(?:\.git)?\/?$/.test(source))
46
+ return source.replace(/\/$/, "");
47
+ return null;
48
+ }
49
+ // SKILL.md at the root, or under skills/<name>/ (and .claude/skills, .agents/skills), matching
50
+ // the layouts explainx and the Vercel skills CLI both use.
51
+ export async function discoverSkills(root) {
52
+ if (await hasSkillMd(root))
53
+ return [{ name: basename(root), dir: root }];
54
+ const found = [];
55
+ for (const base of ["skills", ".claude/skills", ".agents/skills"]) {
56
+ const parent = join(root, base);
57
+ if (!await isDir(parent))
58
+ continue;
59
+ for (const entry of await readdir(parent, { withFileTypes: true })) {
60
+ if (entry.isDirectory() && await hasSkillMd(join(parent, entry.name)))
61
+ found.push({ name: entry.name, dir: join(parent, entry.name) });
62
+ }
63
+ }
64
+ return found;
65
+ }
66
+ export async function addSkill(source, options = {}) {
67
+ const cwd = options.cwd ?? process.cwd();
68
+ const agents = options.allAgents ? ALL_SKILL_AGENTS : options.agent?.length ? options.agent : ["claude-code"];
69
+ for (const a of agents)
70
+ if (!SKILL_TARGETS[a])
71
+ throw new Error(`Unknown agent "${a}". Choose from: ${Object.keys(SKILL_TARGETS).join(", ")}.`);
72
+ const staging = await mkdtemp(join(tmpdir(), "beam-skill-"));
73
+ try {
74
+ let root;
75
+ const local = resolve(cwd, source);
76
+ if (await isDir(local))
77
+ root = local;
78
+ else {
79
+ const url = cloneUrl(source);
80
+ if (!url)
81
+ throw new Error(`"${source}" is neither a local folder nor an owner/repo or https GitHub/GitLab URL.`);
82
+ root = join(staging, "repo");
83
+ // hooksPath=/dev/null: a cloned repo must never get to run its own git hooks.
84
+ await run("git", ["-c", "core.hooksPath=/dev/null", "clone", "--depth", "1", "--quiet", url, root], { timeout: 60_000 });
85
+ }
86
+ let skills = await discoverSkills(root);
87
+ if (options.skill)
88
+ skills = skills.filter(s => s.name === options.skill);
89
+ if (!skills.length)
90
+ throw new Error(options.skill ? `No skill named "${options.skill}" found in ${source}.` : `No SKILL.md found in ${source}.`);
91
+ if (skills.length > 1 && !options.skill && !options.scanOnly) {
92
+ throw new Error(`${source} contains ${skills.length} skills (${skills.map(s => s.name).join(", ")}). Pick one with --skill <name>.`);
93
+ }
94
+ const scans = await Promise.all(skills.map(s => scanSkillDir(s.dir, s.name)));
95
+ const result = { source, scans, installed: [], blocked: [] };
96
+ if (options.scanOnly)
97
+ return result;
98
+ const base = options.global ? (options.home ?? homedir()) : cwd;
99
+ for (const scan of scans) {
100
+ if (isBlocked(scan) && !options.force) {
101
+ result.blocked.push(scan.name);
102
+ continue;
103
+ }
104
+ if (!SAFE_NAME.test(scan.name))
105
+ throw new Error(`Refusing unsafe skill name "${scan.name}".`);
106
+ for (const dest of new Set(agents.map(a => join(base, SKILL_TARGETS[a][options.global ? "global" : "project"], scan.name)))) {
107
+ await mkdir(join(dest, ".."), { recursive: true });
108
+ // Symlinks are dropped so a skill can't point an installed file at something outside itself.
109
+ await cp(scan.dir, dest, { recursive: true, force: true, filter: async (src) => !/[\\/]\.git(?:[\\/]|$)/.test(src) && !(await lstat(src)).isSymbolicLink() });
110
+ result.installed.push({ name: scan.name, path: dest });
111
+ }
112
+ }
113
+ return result;
114
+ }
115
+ finally {
116
+ await rm(staging, { recursive: true, force: true });
117
+ }
118
+ }
119
+ // `beam skills scan` with no path: every skill already installed for the supported agents.
120
+ export async function scanInstalledSkills(home = homedir(), cwd = process.cwd()) {
121
+ const scans = [];
122
+ const seen = new Set();
123
+ for (const base of [home, cwd]) {
124
+ for (const rel of SKILL_DIRS) {
125
+ const parent = join(base, rel);
126
+ if (!await isDir(parent))
127
+ continue;
128
+ for (const entry of await readdir(parent, { withFileTypes: true })) {
129
+ const dir = join(parent, entry.name);
130
+ if (!entry.isDirectory() || seen.has(dir) || !await hasSkillMd(dir))
131
+ continue;
132
+ seen.add(dir);
133
+ scans.push(await scanSkillDir(dir, entry.name));
134
+ }
135
+ }
136
+ }
137
+ return scans;
138
+ }
139
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.js","sourceRoot":"","sources":["../src/skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,YAAY,EAAkB,MAAM,iBAAiB,CAAC;AAE/D,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAEhC,gGAAgG;AAChG,8FAA8F;AAC9F,qFAAqF;AACrF,MAAM,CAAC,MAAM,aAAa,GAAwD;IAChF,aAAa,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,gBAAgB,EAAE;IACtE,KAAK,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,gBAAgB,EAAE;IAC9D,MAAM,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,gBAAgB,EAAE;IAC/D,MAAM,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,gBAAgB,EAAE;IAC/D,QAAQ,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,gBAAgB,EAAE;IACjE,aAAa,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,iBAAiB,EAAE;CACxE,CAAC;AACF,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;AAC3D,0EAA0E;AAC1E,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAKzG,6FAA6F;AAC7F,+FAA+F;AAC/F,kEAAkE;AAClE,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,IAAe,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;AAErL,MAAM,SAAS,GAAG,wCAAwC,CAAC;AAC3D,MAAM,SAAS,GAAG,8BAA8B,CAAC;AAEjD,KAAK,UAAU,KAAK,CAAC,IAAY,IAAI,IAAI,CAAC;IAAC,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;AAAC,CAAC;AAAC,MAAM,CAAC;IAAC,OAAO,KAAK,CAAC;AAAC,CAAC,CAAC,CAAC;AAC/G,KAAK,UAAU,UAAU,CAAC,GAAW,IAAI,IAAI,CAAC;IAAC,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;AAAC,CAAC;AAAC,MAAM,CAAC;IAAC,OAAO,KAAK,CAAC;AAAC,CAAC,CAAC,CAAC;AAE/H,SAAS,QAAQ,CAAC,MAAc;IAC9B,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACtC,IAAI,KAAK;QAAE,OAAO,sBAAsB,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC;IACzF,IAAI,yEAAyE,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC7H,OAAO,IAAI,CAAC;AACd,CAAC;AAED,+FAA+F;AAC/F,2DAA2D;AAC3D,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,IAAY;IAC/C,IAAI,MAAM,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;IACzE,MAAM,KAAK,GAAyC,EAAE,CAAC;IACvD,KAAK,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,gBAAgB,EAAE,gBAAgB,CAAC,EAAE,CAAC;QAClE,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC,MAAM,KAAK,CAAC,MAAM,CAAC;YAAE,SAAS;QACnC,KAAK,MAAM,KAAK,IAAI,MAAM,OAAO,CAAC,MAAM,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YACnE,IAAI,KAAK,CAAC,WAAW,EAAE,IAAI,MAAM,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACzI,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,MAAc,EAAE,UAA2B,EAAE;IAC1E,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IACzC,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAC9G,KAAK,MAAM,CAAC,IAAI,MAAM;QAAE,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,mBAAmB,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAE/I,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,aAAa,CAAC,CAAC,CAAC;IAC7D,IAAI,CAAC;QACH,IAAI,IAAY,CAAC;QACjB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACnC,IAAI,MAAM,KAAK,CAAC,KAAK,CAAC;YAAE,IAAI,GAAG,KAAK,CAAC;aAChC,CAAC;YACJ,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC7B,IAAI,CAAC,GAAG;gBAAE,MAAM,IAAI,KAAK,CAAC,IAAI,MAAM,2EAA2E,CAAC,CAAC;YACjH,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC7B,8EAA8E;YAC9E,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,0BAA0B,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QAC3H,CAAC;QAED,IAAI,MAAM,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,OAAO,CAAC,KAAK;YAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;QACzE,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,mBAAmB,OAAO,CAAC,KAAK,cAAc,MAAM,GAAG,CAAC,CAAC,CAAC,wBAAwB,MAAM,GAAG,CAAC,CAAC;QACjJ,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YAC7D,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,aAAa,MAAM,CAAC,MAAM,YAAY,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;QACvI,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC9E,MAAM,MAAM,GAAmB,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QAC7E,IAAI,OAAO,CAAC,QAAQ;YAAE,OAAO,MAAM,CAAC;QAEpC,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QAChE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;gBAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAAC,SAAS;YAAC,CAAC;YACpF,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC;YAC9F,KAAK,MAAM,IAAI,IAAI,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC5H,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBACnD,6FAA6F;gBAC7F,MAAM,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAC,GAAG,EAAC,EAAE,CAAC,CAAC,uBAAuB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;gBAC5J,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YACzD,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;AACH,CAAC;AAED,2FAA2F;AAC3F,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,IAAI,GAAG,OAAO,EAAE,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IAC7E,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QAC/B,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;YAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAC/B,IAAI,CAAC,MAAM,KAAK,CAAC,MAAM,CAAC;gBAAE,SAAS;YACnC,KAAK,MAAM,KAAK,IAAI,MAAM,OAAO,CAAC,MAAM,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;gBACnE,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;gBACrC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,UAAU,CAAC,GAAG,CAAC;oBAAE,SAAS;gBAC9E,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBACd,KAAK,CAAC,IAAI,CAAC,MAAM,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;YAClD,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { type Event } from "./core.js";
2
+ export interface TamperState {
3
+ expectedHooks: string[];
4
+ updatedAt: string;
5
+ }
6
+ export declare function readTamperState(): Promise<TamperState | null>;
7
+ export declare function recordExpectedHooks(home?: string): Promise<void>;
8
+ export declare function checkForTampering(home?: string): Promise<Event[]>;
9
+ export declare function reportControlEvent(ruleSuffix: string, title: string, detail: string): Promise<void>;