@cirvix_ai/agent-control 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +76 -17
  2. package/bin/cirvix.mjs +539 -85
  3. package/bin/escape-benchmark.mjs +67 -0
  4. package/package.json +36 -16
  5. package/src/adapters/base.mjs +150 -0
  6. package/src/adapters/claude-code.mjs +161 -0
  7. package/src/adapters/cline.mjs +107 -0
  8. package/src/adapters/codex.mjs +104 -0
  9. package/src/adapters/cursor.mjs +104 -0
  10. package/src/adapters/frameworks.mjs +110 -0
  11. package/src/adapters/gemini-cli.mjs +104 -0
  12. package/src/adapters/generic-mcp.mjs +101 -0
  13. package/src/adapters/index.mjs +209 -0
  14. package/src/adapters/roo-code.mjs +106 -0
  15. package/src/adapters/vscode.mjs +104 -0
  16. package/src/adapters/windsurf.mjs +107 -0
  17. package/src/commands/console.mjs +58 -0
  18. package/src/commands/demo.mjs +55 -124
  19. package/src/commands/doctor.mjs +235 -0
  20. package/src/commands/init.mjs +292 -30
  21. package/src/commands/interactive.mjs +690 -0
  22. package/src/commands/kill.mjs +74 -0
  23. package/src/commands/login.mjs +227 -0
  24. package/src/commands/onboard.mjs +52 -0
  25. package/src/commands/passport.mjs +149 -0
  26. package/src/commands/policy.mjs +10 -6
  27. package/src/commands/protect.mjs +293 -0
  28. package/src/commands/prove.mjs +209 -0
  29. package/src/commands/redteam.mjs +51 -0
  30. package/src/commands/scan.mjs +11 -9
  31. package/src/commands/shadow.mjs +62 -0
  32. package/src/commands/simulate.mjs +96 -0
  33. package/src/commands/status.mjs +122 -41
  34. package/src/commands/upgrade.mjs +11 -11
  35. package/src/commands/welcome.mjs +105 -0
  36. package/src/core/authority.mjs +909 -0
  37. package/src/core/baseline.mjs +97 -0
  38. package/src/core/config-store.mjs +280 -0
  39. package/src/core/cost.mjs +0 -0
  40. package/src/core/detect.mjs +4 -33
  41. package/src/core/entitlements.mjs +7 -24
  42. package/src/core/escape-benchmark.mjs +597 -0
  43. package/src/core/events.mjs +234 -0
  44. package/src/core/evidence.mjs +212 -0
  45. package/src/core/format.mjs +44 -18
  46. package/src/core/gateway.mjs +15 -211
  47. package/src/core/graph.mjs +270 -0
  48. package/src/core/guard.mjs +118 -4
  49. package/src/core/intent.mjs +166 -0
  50. package/src/core/journal.mjs +131 -40
  51. package/src/core/kill-switch.mjs +122 -0
  52. package/src/core/notices.mjs +22 -2
  53. package/src/core/packs.mjs +193 -0
  54. package/src/core/passport.mjs +555 -0
  55. package/src/core/pipeline.mjs +148 -6
  56. package/src/core/prompts.mjs +51 -0
  57. package/src/core/proof.mjs +440 -0
  58. package/src/core/redteam/index.mjs +185 -0
  59. package/src/core/referral.mjs +187 -0
  60. package/src/core/sandbox.mjs +139 -0
  61. package/src/core/session.mjs +172 -0
  62. package/src/core/shadow.mjs +95 -0
  63. package/src/core/theme.mjs +240 -0
  64. package/src/core/trifecta.mjs +321 -0
  65. package/src/core/ui/controller.mjs +192 -0
  66. package/src/core/ui/decisions.mjs +55 -0
  67. package/src/core/ui/index.mjs +49 -0
  68. package/src/core/ui/intercept.mjs +103 -0
  69. package/src/core/ui/live.mjs +51 -0
  70. package/src/core/ui/primitives.mjs +123 -0
  71. package/src/core/ui/theme.mjs +92 -0
  72. package/src/core/verified.mjs +108 -0
  73. package/src/core/windows.mjs +270 -0
  74. package/src/index.mjs +67 -0
  75. package/src/tui/activity.mjs +71 -0
  76. package/src/tui/app.mjs +292 -0
  77. package/src/tui/cards.mjs +235 -0
  78. package/src/tui/composer.mjs +88 -0
  79. package/src/tui/palette.mjs +48 -0
  80. package/src/tui/status.mjs +42 -0
  81. package/src/core/cinematic.mjs +0 -545
@@ -24,10 +24,18 @@
24
24
  * The limit notice fires on the transition into the limit, not on every
25
25
  * refused call after it — a process that keeps calling past its quota would
26
26
  * otherwise print the same paragraph hundreds of times. The nudge is once per
27
- * day and `Meter` owns that flag.
27
+ * day and `Meter` owns that flag. The domain-signal prompt is once per
28
+ * process, on the first egress-class denial: the story it offers to share is
29
+ * worth telling once, not every time an agent reaches for a credential file.
28
30
  */
29
31
 
30
- import { agentLimitReached, quotaReached, softNudge } from "./prompts.mjs";
32
+ import {
33
+ agentLimitReached,
34
+ domainSignal,
35
+ quotaReached,
36
+ softNudge,
37
+ EGRESS_DENY_RULES,
38
+ } from "./prompts.mjs";
31
39
 
32
40
  /**
33
41
  * Builds the per-decision notice hook.
@@ -43,6 +51,7 @@ export function commercialNotices({ licence, meter, write }) {
43
51
 
44
52
  let saidQuota = false;
45
53
  let saidAgents = false;
54
+ let saidDomain = false;
46
55
 
47
56
  return function notice(decision) {
48
57
  if (!decision) return;
@@ -54,6 +63,17 @@ export function commercialNotices({ licence, meter, write }) {
54
63
  // this whole file exists to correct.
55
64
  const rule = decision.rule ?? decision.policy;
56
65
 
66
+ if (decision.verdict === "deny" && EGRESS_DENY_RULES.has(rule)) {
67
+ if (saidDomain) return;
68
+ saidDomain = true;
69
+ const text = domainSignal(licence, {
70
+ rule,
71
+ destination: decision.destination ?? "",
72
+ });
73
+ if (text) write(`\n${text}\n`);
74
+ return;
75
+ }
76
+
57
77
  if (rule === "quota-exhausted") {
58
78
  if (saidQuota) return;
59
79
  saidQuota = true;
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Policy packs — installable, forkable, provenanced rule sets.
3
+ *
4
+ * The four files in `policies/` were already packs in everything but name: a
5
+ * curated set of rules with a stated purpose. What they lacked was the
6
+ * metadata that makes a pack shareable — who wrote it, what version it is,
7
+ * what it is for, what it was forked from, and whether the rules still match
8
+ * the hash somebody reviewed.
9
+ *
10
+ * WHY THE MANIFEST LIVES IN COMMENTS
11
+ * ----------------------------------
12
+ * A pack is a `.policy` file and nothing else. The manifest is carried on
13
+ * `#@ key: value` lines, which the DSL parser already skips as comments.
14
+ *
15
+ * The alternative — a sidecar `.pack.json` — was rejected because it splits a
16
+ * pack into two files that can drift apart, and the failure mode of that drift
17
+ * is a manifest describing rules that are no longer there. One file cannot
18
+ * disagree with itself about what it contains.
19
+ *
20
+ * WHAT THE HASH COVERS
21
+ * --------------------
22
+ * The rules, not the manifest. Bumping a description or adding an author must
23
+ * not invalidate a review of the rules, and editing a single `deny` must
24
+ * invalidate it. Hashing the whole file would get both of those backwards.
25
+ */
26
+
27
+ import { createHash } from "node:crypto";
28
+
29
+ /** Manifest fields a pack may declare. Anything else is ignored, not an error:
30
+ * a pack written against a newer Cirvix must still install on an older one. */
31
+ const FIELDS = new Set([
32
+ "id", "name", "version", "description", "author", "homepage",
33
+ "risk", "targets", "requires", "forked-from", "forked-at", "official",
34
+ ]);
35
+
36
+ const MANIFEST_LINE = /^#@\s*([a-z-]+)\s*:\s*(.*)$/i;
37
+
38
+ /**
39
+ * Split a pack file into its manifest and its rule text.
40
+ *
41
+ * The rule text is returned verbatim, including comments that are not manifest
42
+ * lines, because those comments are the author explaining their reasoning and
43
+ * dropping them would make an installed pack less useful than the one on disk.
44
+ */
45
+ export function parsePack(text, { source = null } = {}) {
46
+ const manifest = {};
47
+ const ruleLines = [];
48
+
49
+ for (const line of String(text ?? "").split(/\r?\n/)) {
50
+ const m = line.match(MANIFEST_LINE);
51
+ if (m && FIELDS.has(m[1].toLowerCase())) {
52
+ const key = m[1].toLowerCase();
53
+ const value = m[2].trim();
54
+ if (key === "targets" || key === "requires") {
55
+ manifest[key] = value.split(/[,\s]+/).filter(Boolean);
56
+ } else if (key === "official") {
57
+ manifest[key] = value === "true";
58
+ } else {
59
+ manifest[key] = value;
60
+ }
61
+ continue;
62
+ }
63
+ ruleLines.push(line);
64
+ }
65
+
66
+ const rules = ruleLines.join("\n");
67
+ return {
68
+ manifest: {
69
+ id: manifest.id ?? null,
70
+ name: manifest.name ?? manifest.id ?? "untitled pack",
71
+ version: manifest.version ?? "0.0.0",
72
+ description: manifest.description ?? "",
73
+ author: manifest.author ?? "unknown",
74
+ risk: manifest.risk ?? "unspecified",
75
+ targets: manifest.targets ?? [],
76
+ requires: manifest.requires ?? [],
77
+ official: manifest.official ?? false,
78
+ forkedFrom: manifest["forked-from"] ?? null,
79
+ forkedAt: manifest["forked-at"] ?? null,
80
+ homepage: manifest.homepage ?? null,
81
+ source,
82
+ },
83
+ rules,
84
+ hash: hashRules(rules),
85
+ };
86
+ }
87
+
88
+ /** sha256 over the rule text with trailing whitespace normalised, so a pack
89
+ * that survives a round trip through an editor still verifies. */
90
+ export function hashRules(rules) {
91
+ const normalised = String(rules ?? "")
92
+ .split(/\r?\n/)
93
+ .map((l) => l.replace(/\s+$/, ""))
94
+ .join("\n")
95
+ .replace(/\n+$/, "\n");
96
+ return "sha256:" + createHash("sha256").update(normalised, "utf8").digest("hex");
97
+ }
98
+
99
+ /** Does this pack still contain the rules somebody reviewed? */
100
+ export function verifyPack(pack, expectedHash) {
101
+ const actual = hashRules(pack.rules);
102
+ return { ok: actual === expectedHash, expected: expectedHash, actual };
103
+ }
104
+
105
+ /**
106
+ * Render a pack back to a file.
107
+ *
108
+ * Manifest first, then the rules exactly as they were. Round-tripping a pack
109
+ * through parse → render must not change its hash, which the tests assert.
110
+ */
111
+ export function renderPack(pack) {
112
+ const m = pack.manifest;
113
+ const head = [
114
+ `#@ id: ${m.id}`,
115
+ `#@ name: ${m.name}`,
116
+ `#@ version: ${m.version}`,
117
+ m.description ? `#@ description: ${m.description}` : null,
118
+ `#@ author: ${m.author}`,
119
+ `#@ risk: ${m.risk}`,
120
+ m.targets?.length ? `#@ targets: ${m.targets.join(", ")}` : null,
121
+ m.requires?.length ? `#@ requires: ${m.requires.join(", ")}` : null,
122
+ m.official ? `#@ official: true` : null,
123
+ m.forkedFrom ? `#@ forked-from: ${m.forkedFrom}` : null,
124
+ m.forkedAt ? `#@ forked-at: ${m.forkedAt}` : null,
125
+ m.homepage ? `#@ homepage: ${m.homepage}` : null,
126
+ ].filter(Boolean);
127
+ return head.join("\n") + "\n" + pack.rules.replace(/^\n+/, "\n");
128
+ }
129
+
130
+ /**
131
+ * Fork a pack under a new identity, keeping provenance.
132
+ *
133
+ * The fork records the parent's id AND the parent's rule hash. The id alone
134
+ * would be a claim; the hash is what lets anyone check which version of the
135
+ * parent this actually came from, including after the parent has moved on.
136
+ */
137
+ export function forkPack(pack, { id, name, author = "unknown", now = new Date() } = {}) {
138
+ if (!id) throw new Error("A fork needs an id.");
139
+ return {
140
+ manifest: {
141
+ ...pack.manifest,
142
+ id,
143
+ name: name ?? `${pack.manifest.name} (fork)`,
144
+ version: "0.1.0",
145
+ author,
146
+ official: false,
147
+ forkedFrom: `${pack.manifest.id ?? "unknown"}@${hashRules(pack.rules)}`,
148
+ forkedAt: now.toISOString(),
149
+ },
150
+ rules: pack.rules,
151
+ hash: hashRules(pack.rules),
152
+ };
153
+ }
154
+
155
+ /**
156
+ * The difference between two packs, as rule names.
157
+ *
158
+ * Reported by name rather than by line, because a pack update that reorders
159
+ * rules or rewrites a comment is not a change anyone needs to review, and a
160
+ * line diff would present it as though it were.
161
+ */
162
+ export function diffPacks(before, after) {
163
+ const names = (p) => {
164
+ const out = [];
165
+ for (const m of String(p?.rules ?? "").matchAll(/^\s*name\s*=\s*(\S+)/gm)) out.push(m[1]);
166
+ return out;
167
+ };
168
+ const a = new Set(names(before));
169
+ const b = new Set(names(after));
170
+ return {
171
+ added: [...b].filter((n) => !a.has(n)),
172
+ removed: [...a].filter((n) => !b.has(n)),
173
+ kept: [...a].filter((n) => b.has(n)),
174
+ changed: hashRules(before?.rules) !== hashRules(after?.rules),
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Sort packs for a listing.
180
+ *
181
+ * Official first, then by risk posture descending, then by name. A developer
182
+ * scanning this list is looking for "the safe default written by the vendor",
183
+ * and that should not be somewhere in the middle alphabetically.
184
+ */
185
+ const RISK_WEIGHT = { strict: 3, balanced: 2, permissive: 1, unspecified: 0 };
186
+ export function sortPacks(packs) {
187
+ return [...packs].sort((x, y) => {
188
+ if (x.manifest.official !== y.manifest.official) return x.manifest.official ? -1 : 1;
189
+ const rw = (RISK_WEIGHT[y.manifest.risk] ?? 0) - (RISK_WEIGHT[x.manifest.risk] ?? 0);
190
+ if (rw) return rw;
191
+ return String(x.manifest.name).localeCompare(String(y.manifest.name));
192
+ });
193
+ }