@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.
- package/README.md +76 -17
- package/bin/cirvix.mjs +539 -85
- package/bin/escape-benchmark.mjs +67 -0
- package/package.json +36 -16
- package/src/adapters/base.mjs +150 -0
- package/src/adapters/claude-code.mjs +161 -0
- package/src/adapters/cline.mjs +107 -0
- package/src/adapters/codex.mjs +104 -0
- package/src/adapters/cursor.mjs +104 -0
- package/src/adapters/frameworks.mjs +110 -0
- package/src/adapters/gemini-cli.mjs +104 -0
- package/src/adapters/generic-mcp.mjs +101 -0
- package/src/adapters/index.mjs +209 -0
- package/src/adapters/roo-code.mjs +106 -0
- package/src/adapters/vscode.mjs +104 -0
- package/src/adapters/windsurf.mjs +107 -0
- package/src/commands/console.mjs +58 -0
- package/src/commands/demo.mjs +55 -124
- package/src/commands/doctor.mjs +235 -0
- package/src/commands/init.mjs +292 -30
- package/src/commands/interactive.mjs +690 -0
- package/src/commands/kill.mjs +74 -0
- package/src/commands/login.mjs +227 -0
- package/src/commands/onboard.mjs +52 -0
- package/src/commands/passport.mjs +149 -0
- package/src/commands/policy.mjs +10 -6
- package/src/commands/protect.mjs +293 -0
- package/src/commands/prove.mjs +209 -0
- package/src/commands/redteam.mjs +51 -0
- package/src/commands/scan.mjs +11 -9
- package/src/commands/shadow.mjs +62 -0
- package/src/commands/simulate.mjs +96 -0
- package/src/commands/status.mjs +122 -41
- package/src/commands/upgrade.mjs +11 -11
- package/src/commands/welcome.mjs +105 -0
- package/src/core/authority.mjs +909 -0
- package/src/core/baseline.mjs +97 -0
- package/src/core/config-store.mjs +280 -0
- package/src/core/cost.mjs +0 -0
- package/src/core/detect.mjs +4 -33
- package/src/core/entitlements.mjs +7 -24
- package/src/core/escape-benchmark.mjs +597 -0
- package/src/core/events.mjs +234 -0
- package/src/core/evidence.mjs +212 -0
- package/src/core/format.mjs +44 -18
- package/src/core/gateway.mjs +15 -211
- package/src/core/graph.mjs +270 -0
- package/src/core/guard.mjs +118 -4
- package/src/core/intent.mjs +166 -0
- package/src/core/journal.mjs +131 -40
- package/src/core/kill-switch.mjs +122 -0
- package/src/core/notices.mjs +22 -2
- package/src/core/packs.mjs +193 -0
- package/src/core/passport.mjs +555 -0
- package/src/core/pipeline.mjs +148 -6
- package/src/core/prompts.mjs +51 -0
- package/src/core/proof.mjs +440 -0
- package/src/core/redteam/index.mjs +185 -0
- package/src/core/referral.mjs +187 -0
- package/src/core/sandbox.mjs +139 -0
- package/src/core/session.mjs +172 -0
- package/src/core/shadow.mjs +95 -0
- package/src/core/theme.mjs +240 -0
- package/src/core/trifecta.mjs +321 -0
- package/src/core/ui/controller.mjs +192 -0
- package/src/core/ui/decisions.mjs +55 -0
- package/src/core/ui/index.mjs +49 -0
- package/src/core/ui/intercept.mjs +103 -0
- package/src/core/ui/live.mjs +51 -0
- package/src/core/ui/primitives.mjs +123 -0
- package/src/core/ui/theme.mjs +92 -0
- package/src/core/verified.mjs +108 -0
- package/src/core/windows.mjs +270 -0
- package/src/index.mjs +67 -0
- package/src/tui/activity.mjs +71 -0
- package/src/tui/app.mjs +292 -0
- package/src/tui/cards.mjs +235 -0
- package/src/tui/composer.mjs +88 -0
- package/src/tui/palette.mjs +48 -0
- package/src/tui/status.mjs +42 -0
- package/src/core/cinematic.mjs +0 -545
package/src/core/notices.mjs
CHANGED
|
@@ -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 {
|
|
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
|
+
}
|