acuvo-code 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/CHANGELOG.md +328 -0
- package/ENTERPRISE.md +927 -0
- package/LICENSE +120 -0
- package/README.md +1245 -0
- package/ROADMAP.md +556 -0
- package/bin/acuvo-mcp.mjs +208 -0
- package/bin/acuvo.mjs +3108 -0
- package/lib/acceptance-consent.mjs +168 -0
- package/lib/acceptance.mjs +859 -0
- package/lib/account.mjs +226 -0
- package/lib/acuvo-dir.mjs +72 -0
- package/lib/acuvo-models.mjs +141 -0
- package/lib/apply-patch.mjs +570 -0
- package/lib/ask-user.mjs +173 -0
- package/lib/audit.mjs +530 -0
- package/lib/auto-lease.mjs +174 -0
- package/lib/background.mjs +842 -0
- package/lib/best-of.mjs +334 -0
- package/lib/board.mjs +232 -0
- package/lib/breaker.mjs +93 -0
- package/lib/budget.mjs +1375 -0
- package/lib/builtin-skills.mjs +135 -0
- package/lib/cache-floor.mjs +204 -0
- package/lib/chain.mjs +303 -0
- package/lib/changed-paths.mjs +84 -0
- package/lib/chat.mjs +434 -0
- package/lib/checkpoint.mjs +637 -0
- package/lib/child-lifetime.mjs +71 -0
- package/lib/cli-args.mjs +1255 -0
- package/lib/code-review.mjs +1382 -0
- package/lib/colour.mjs +82 -0
- package/lib/command.mjs +2847 -0
- package/lib/compact.mjs +1151 -0
- package/lib/completion.mjs +515 -0
- package/lib/creative-engines.mjs +779 -0
- package/lib/db-inspect.mjs +1624 -0
- package/lib/delete.mjs +111 -0
- package/lib/design-loop.mjs +570 -0
- package/lib/diff-preview.mjs +1044 -0
- package/lib/doctor.mjs +2139 -0
- package/lib/dropped.mjs +216 -0
- package/lib/edit-diagnostics.mjs +277 -0
- package/lib/edit.mjs +460 -0
- package/lib/env-file.mjs +250 -0
- package/lib/escalate.mjs +702 -0
- package/lib/evaluate.mjs +284 -0
- package/lib/fetch-text.mjs +952 -0
- package/lib/fleet-budget.mjs +256 -0
- package/lib/gh.mjs +1536 -0
- package/lib/git.mjs +1341 -0
- package/lib/github.mjs +261 -0
- package/lib/h2.mjs +194 -0
- package/lib/handoff.mjs +417 -0
- package/lib/hooks.mjs +626 -0
- package/lib/http-probe.mjs +907 -0
- package/lib/image-director.mjs +322 -0
- package/lib/image-edit.mjs +522 -0
- package/lib/imagegen.mjs +998 -0
- package/lib/interrupt.mjs +234 -0
- package/lib/learned.mjs +353 -0
- package/lib/lease-watch.mjs +115 -0
- package/lib/lease.mjs +868 -0
- package/lib/localize.mjs +834 -0
- package/lib/log-tail.mjs +1052 -0
- package/lib/login.mjs +157 -0
- package/lib/lsp.mjs +1613 -0
- package/lib/mcp-consent.mjs +377 -0
- package/lib/mcp-defaults.mjs +780 -0
- package/lib/mcp-server.mjs +1343 -0
- package/lib/mcp.mjs +1263 -0
- package/lib/media.mjs +1283 -0
- package/lib/memory-workspace.mjs +179 -0
- package/lib/model-json.mjs +132 -0
- package/lib/model-tier.mjs +171 -0
- package/lib/model.mjs +1445 -0
- package/lib/parallel.mjs +144 -0
- package/lib/perchance.mjs +210 -0
- package/lib/plan-coherence.mjs +1461 -0
- package/lib/plan-ledger.mjs +981 -0
- package/lib/plan.mjs +461 -0
- package/lib/policy.mjs +783 -0
- package/lib/prefix-order.mjs +38 -0
- package/lib/project-memory.mjs +127 -0
- package/lib/prompt.mjs +109 -0
- package/lib/python.mjs +862 -0
- package/lib/rcfile.mjs +853 -0
- package/lib/read-window.mjs +743 -0
- package/lib/refute-tools.mjs +34 -0
- package/lib/refute.mjs +806 -0
- package/lib/repl-driver.mjs +264 -0
- package/lib/repl.mjs +324 -0
- package/lib/replay.mjs +1218 -0
- package/lib/repo-map.mjs +1101 -0
- package/lib/report.mjs +419 -0
- package/lib/search-rank.mjs +99 -0
- package/lib/search.mjs +659 -0
- package/lib/secret-paths.mjs +54 -0
- package/lib/session.mjs +1017 -0
- package/lib/skills.mjs +703 -0
- package/lib/slash.mjs +356 -0
- package/lib/spawn-argv.mjs +1151 -0
- package/lib/spend.mjs +250 -0
- package/lib/steer.mjs +280 -0
- package/lib/stream.mjs +253 -0
- package/lib/stuck.mjs +712 -0
- package/lib/subagent.mjs +749 -0
- package/lib/terminal-graphics.mjs +171 -0
- package/lib/tool-prefix.mjs +226 -0
- package/lib/tool-shortlist.mjs +162 -0
- package/lib/tools.mjs +2333 -0
- package/lib/tsserver.mjs +423 -0
- package/lib/turn.mjs +5672 -0
- package/lib/untrusted-block.mjs +271 -0
- package/lib/verify-claim.mjs +299 -0
- package/lib/vision.mjs +330 -0
- package/lib/voice-task.mjs +561 -0
- package/lib/warm-provider.mjs +255 -0
- package/lib/websearch.mjs +401 -0
- package/lib/workspace.mjs +928 -0
- package/lib/write-approval.mjs +235 -0
- package/lib/write-many.mjs +162 -0
- package/package.json +62 -0
- package/scripts/bundle.mjs +768 -0
- package/scripts/cache-floor.mjs +176 -0
- package/scripts/machine.mjs +226 -0
- package/scripts/test.mjs +139 -0
- package/skills/accessibility.md +87 -0
- package/skills/acuvo-design-system.md +123 -0
- package/skills/animation.md +84 -0
- package/skills/api-design.md +82 -0
- package/skills/auth-and-sessions.md +78 -0
- package/skills/build-with-a-framework.md +101 -0
- package/skills/colour-and-contrast.md +112 -0
- package/skills/creative-engines.md +81 -0
- package/skills/css-layout.md +85 -0
- package/skills/data-and-charts.md +77 -0
- package/skills/debugging.md +76 -0
- package/skills/designing-by-looking.md +84 -0
- package/skills/error-handling.md +78 -0
- package/skills/forms-and-validation.md +93 -0
- package/skills/nextjs-app-router.md +75 -0
- package/skills/page-composition.md +103 -0
- package/skills/performance.md +77 -0
- package/skills/plan-before-building.md +52 -0
- package/skills/planning-and-delegating.md +72 -0
- package/skills/refactoring.md +70 -0
- package/skills/security-basics.md +76 -0
- package/skills/state-management.md +73 -0
- package/skills/supabase-multitenant.md +72 -0
- package/skills/typescript-strict.md +90 -0
- package/skills/typography.md +135 -0
- package/skills/verify-your-own-work.md +62 -0
- package/skills/web-app-quality.md +62 -0
- package/skills/working-in-the-background.md +64 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ── ⚠️⚠️ A CLONED REPOSITORY COULD RUN CODE, AND IT WAS REPORTED AS A ✔ ──────
|
|
3
|
+
*
|
|
4
|
+
* PROVEN 2026-08-13 in a scratch workspace, not argued:
|
|
5
|
+
*
|
|
6
|
+
* .acuvo/acceptance.json → {"criteria":[{"command":"node payload.js"}]}
|
|
7
|
+
* payload.js → writeFileSync('PWNED.txt', …)
|
|
8
|
+
*
|
|
9
|
+
* $ acuvo "say hello, change nothing"
|
|
10
|
+
* ✔ MET — `node payload.js` exited 0
|
|
11
|
+
* $ cat PWNED.txt
|
|
12
|
+
* a cloned repo executed this without being asked
|
|
13
|
+
*
|
|
14
|
+
* No prompt. No record. And the CLI printed the payload's execution as a
|
|
15
|
+
* PASSING CHECK — the most reassuring possible rendering of an attack.
|
|
16
|
+
*
|
|
17
|
+
* ⭐ This is the SAME hole `mcp-consent.mjs` was written to close, one file
|
|
18
|
+
* over. Its header says it exactly: *"Cloning an untrusted repository and typing
|
|
19
|
+
* `acuvo` used to execute a binary that repository chose — no prompt, no record,
|
|
20
|
+
* full environment. Consent, not a blocklist: a committed `.mcp.json` is a
|
|
21
|
+
* legitimate thing to want. What was missing is that nobody ever agreed to it."*
|
|
22
|
+
* Every word applies here, and `acceptance.json` was simply missed.
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ THE ALLOWLIST IS NOT THE ANSWER, THOUGH IT LOOKS LIKE ONE. `command.mjs`
|
|
25
|
+
* only permits vetted binaries, so this cannot run `curl`. It can run `node`,
|
|
26
|
+
* and `node <a file in the repo>` is arbitrary code — the allowlist bounds the
|
|
27
|
+
* BINARY, never the program. The probe above used nothing but `node`.
|
|
28
|
+
*
|
|
29
|
+
* ── ⭐ CONSENT ON AUTHORSHIP, WHICH IS WHY THIS DOES NOT BECOME A NAG ────────
|
|
30
|
+
*
|
|
31
|
+
* A prompt everybody clicks through is worse than no prompt, because it
|
|
32
|
+
* launders the decision. So the ordinary path must never ask:
|
|
33
|
+
*
|
|
34
|
+
* · the agent calls `declare_acceptance` during YOUR run → that is your own
|
|
35
|
+
* session authoring the file, so trust is recorded at the moment of writing
|
|
36
|
+
* and the next run recognises it silently.
|
|
37
|
+
* · a file that arrived any other way — a clone, a pull, a colleague — is
|
|
38
|
+
* unrecognised, and THAT is the only case that asks.
|
|
39
|
+
*
|
|
40
|
+
* The result: a person who uses this feature normally never sees a question,
|
|
41
|
+
* and the question they do see means something specific.
|
|
42
|
+
*
|
|
43
|
+
* ── ⚠️ AND WHEN IT IS NOT TRUSTED, THE CRITERIA ARE IGNORED, NOT FATAL ──────
|
|
44
|
+
*
|
|
45
|
+
* Refusing to run at all would make a stray file a denial of service, and would
|
|
46
|
+
* teach people to delete the safety feature. Unrecognised criteria are simply
|
|
47
|
+
* not loaded: the run proceeds exactly as it would in a workspace with no
|
|
48
|
+
* acceptance file, and says so once. Nothing untrusted is executed, and nothing
|
|
49
|
+
* legitimate is blocked.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { fingerprint as hashOf, loadTrust, isTrusted, recordTrust, trustStorePath } from './mcp-consent.mjs';
|
|
53
|
+
|
|
54
|
+
/** `ACUVO_TRUST_ACCEPTANCE=1` — the non-interactive escape, mirroring MCP's. */
|
|
55
|
+
export const TRUST_ENV = 'ACUVO_TRUST_ACCEPTANCE';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* What is being approved: the COMMANDS, not the file.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ Reusing `mcp-consent.mjs`'s hash by shaping criteria into the same
|
|
61
|
+
* `{name, command, args}` form it canonicalises. Reformatting the JSON or
|
|
62
|
+
* editing a `why` must not re-prompt — a nag people learn to click through is
|
|
63
|
+
* the failure mode — while changing a COMMAND must. The `kind` prefix keeps an
|
|
64
|
+
* acceptance fingerprint from ever colliding with an MCP one in the shared
|
|
65
|
+
* store, so approving a server can never silently approve a criterion.
|
|
66
|
+
*/
|
|
67
|
+
export function acceptanceFingerprint(criteria) {
|
|
68
|
+
const shaped = (criteria ?? [])
|
|
69
|
+
.map((c) => ({ name: 'acceptance', command: String(c?.command ?? ''), args: [] }))
|
|
70
|
+
.sort((a, b) => a.command.localeCompare(b.command));
|
|
71
|
+
return `acceptance:${hashOf(shaped)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The text a person reads before deciding. Exported so a test can assert it names the command. */
|
|
75
|
+
export function describeCriteria(criteria, { root = '' } = {}) {
|
|
76
|
+
const lines = [
|
|
77
|
+
'',
|
|
78
|
+
` ⚠️ ${root || 'this workspace'} ships ${criteria.length} acceptance criterion${criteria.length === 1 ? '' : 's'}`,
|
|
79
|
+
' that nobody in this workspace has approved. Each one is a COMMAND that will be',
|
|
80
|
+
' run on your machine, and a passing run is reported as a ✔.',
|
|
81
|
+
'',
|
|
82
|
+
];
|
|
83
|
+
for (const c of criteria) {
|
|
84
|
+
lines.push(` $ ${c?.command ?? '(no command)'}`);
|
|
85
|
+
if (c?.why) lines.push(` (${c.why})`);
|
|
86
|
+
}
|
|
87
|
+
lines.push('');
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* May these criteria be executed?
|
|
93
|
+
*
|
|
94
|
+
* @param {Array<{command?: string, why?: string}>} criteria
|
|
95
|
+
* @param {object} opts
|
|
96
|
+
* @param {string} [opts.root]
|
|
97
|
+
* @param {null | ((q: string) => Promise<string|null>)} [opts.ask]
|
|
98
|
+
* @param {boolean} [opts.isInteractive]
|
|
99
|
+
* @returns {Promise<{allowed: boolean, reason: string, fingerprint: string, remember?: boolean}>}
|
|
100
|
+
*/
|
|
101
|
+
export async function checkAcceptanceConsent(criteria, {
|
|
102
|
+
root = '',
|
|
103
|
+
ask = null,
|
|
104
|
+
isInteractive = false,
|
|
105
|
+
env = process.env,
|
|
106
|
+
home = undefined,
|
|
107
|
+
} = {}) {
|
|
108
|
+
const list = Array.isArray(criteria) ? criteria.filter((c) => c && typeof c.command === 'string' && c.command.trim()) : [];
|
|
109
|
+
const fp = acceptanceFingerprint(list);
|
|
110
|
+
const base = { fingerprint: fp };
|
|
111
|
+
|
|
112
|
+
// Nothing to run is nothing to approve — and must not print a question.
|
|
113
|
+
if (list.length === 0) return { ...base, allowed: true, reason: 'no criteria' };
|
|
114
|
+
|
|
115
|
+
if (String(env[TRUST_ENV] ?? '').trim() === '1') {
|
|
116
|
+
return { ...base, allowed: true, reason: `${TRUST_ENV}=1 was set, so the criteria were accepted without asking` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const store = loadTrust({ env, ...(home === undefined ? {} : { home }) });
|
|
120
|
+
if (isTrusted(fp, store)) {
|
|
121
|
+
return { ...base, allowed: true, reason: 'these exact criteria were approved in this workspace before' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* ⚠️ FAIL CLOSED. In CI, a pipe or a task runner there is nobody to ask, and
|
|
126
|
+
* "nobody objected" is not consent. The run continues with no criteria rather
|
|
127
|
+
* than executing commands nobody agreed to — see the header for why this is
|
|
128
|
+
* not fatal.
|
|
129
|
+
*/
|
|
130
|
+
if (!isInteractive || typeof ask !== 'function') {
|
|
131
|
+
return {
|
|
132
|
+
...base,
|
|
133
|
+
allowed: false,
|
|
134
|
+
reason: 'this workspace ships acceptance criteria that have not been approved, and there is no terminal here to ask.\n'
|
|
135
|
+
+ ` They were NOT run. Approve them interactively once, or set ${TRUST_ENV}=1 if you have read them and this is your own repository.`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const answer = String(await ask(' Run these commands? [y/N] ') ?? '').trim().toLowerCase();
|
|
140
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
141
|
+
return { ...base, allowed: false, reason: 'declined — the criteria were not run' };
|
|
142
|
+
}
|
|
143
|
+
return { ...base, allowed: true, remember: true, reason: 'approved for this workspace' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Record consent because THIS session authored the criteria.
|
|
148
|
+
*
|
|
149
|
+
* ⭐ Called when `declare_acceptance` writes the file. The user's own run
|
|
150
|
+
* created it, so asking about it afterwards would be asking them to approve
|
|
151
|
+
* their own instruction — the definition of a prompt people learn to ignore.
|
|
152
|
+
*
|
|
153
|
+
* ⚠️ Never fatal, exactly like `recordTrust`: failing to remember means asking
|
|
154
|
+
* once next time, which is annoying and safe.
|
|
155
|
+
*/
|
|
156
|
+
export function trustAuthoredCriteria(criteria, { root = '', env = process.env, home = undefined } = {}) {
|
|
157
|
+
const list = Array.isArray(criteria) ? criteria.filter((c) => c && typeof c.command === 'string' && c.command.trim()) : [];
|
|
158
|
+
if (list.length === 0) return { ok: true, already: true };
|
|
159
|
+
const fp = acceptanceFingerprint(list);
|
|
160
|
+
return recordTrust(fp, {
|
|
161
|
+
root,
|
|
162
|
+
servers: list.map((c) => ({ name: 'acceptance', command: c.command, args: [] })),
|
|
163
|
+
env,
|
|
164
|
+
...(home === undefined ? {} : { home }),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export { trustStorePath };
|