great-cto 3.75.4 → 3.75.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/assets/statusline.mjs +7 -4
- package/board/.claude-plugin/plugin.json +1 -1
- package/board/agents/architect.md +19 -1
- package/board/agents/code-reviewer.md +50 -42
- package/board/agents/design-advisor.md +13 -2
- package/board/agents/devops.md +40 -1
- package/board/agents/qa-engineer.md +33 -0
- package/board/agents/security-officer.md +15 -7
- package/board/agents/senior-dev.md +16 -0
- package/board/packages/board/lib/bd-read-worker.mjs +2 -1
- package/board/packages/board/lib/receipt-read-worker.mjs +2 -1
- package/board/scripts/hooks/auto-attach-reviewers.mjs +2 -2
- package/board/scripts/lib/affected-tests.mjs +2 -2
- package/board/scripts/lib/agent-events.mjs +2 -1
- package/board/scripts/lib/agent-usage.mjs +2 -1
- package/board/scripts/lib/approval-level.mjs +3 -1
- package/board/scripts/lib/board-watch.mjs +2 -2
- package/board/scripts/lib/change-tier.mjs +2 -1
- package/board/scripts/lib/cost-meter.mjs +2 -2
- package/board/scripts/lib/cross-model-review.mjs +2 -2
- package/board/scripts/lib/dag-metric.mjs +2 -1
- package/board/scripts/lib/gate-plan.mjs +2 -2
- package/board/scripts/lib/gate-tier.mjs +2 -1
- package/board/scripts/lib/is-main.mjs +24 -0
- package/board/scripts/lib/judge-model.mjs +2 -1
- package/board/scripts/lib/pipeline-health.mjs +2 -1
- package/board/scripts/lib/receipt.mjs +138 -19
- package/board/scripts/lib/second-opinion.mjs +2 -1
- package/board/scripts/lib/session-usage.mjs +2 -1
- package/board/scripts/lib/stack-capabilities.mjs +3 -1
- package/board/scripts/lib/stand-down.mjs +2 -1
- package/board/scripts/lib/system-map.mjs +2 -1
- package/board/scripts/lib/verdict-record.mjs +15 -3
- package/board/scripts/lib/verdict-seal.mjs +95 -0
- package/board/scripts/skill-lint.mjs +2 -1
- package/package.json +1 -1
- package/postinstall.mjs +7 -2
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seals on verdict lines and receipt acceptances — tamper-evidence, not a lock.
|
|
3
|
+
*
|
|
4
|
+
* great_cto-dv3b. A deploy trusts the newest approving verdict in
|
|
5
|
+
* .great_cto/verdicts/<agent>.log and an operator's acceptance in
|
|
6
|
+
* .great_cto/.receipt-accept. Both were plain JSON: a line appended with `echo`,
|
|
7
|
+
* or a field edited by hand, read exactly like one log-verdict.sh wrote.
|
|
8
|
+
*
|
|
9
|
+
* Each now carries an HMAC-SHA256 over the fields that matter, keyed by
|
|
10
|
+
* ~/.great_cto/keys/verdict-seal.key (0600, created on first use). The writers
|
|
11
|
+
* — log-verdict.sh via verdict-record.mjs, and `receipt.mjs --accept` — seal what
|
|
12
|
+
* they write; `receipt.mjs --verify --require` checks before it trusts.
|
|
13
|
+
*
|
|
14
|
+
* What it does NOT do, said rather than implied (ADR-046). Every agent runs as the
|
|
15
|
+
* same OS user as the operator, so no file on this machine is out of an agent's
|
|
16
|
+
* reach: code that imports this module can seal anything. What the seal changes:
|
|
17
|
+
* a hand-written or hand-edited line no longer passes, and getting a valid one
|
|
18
|
+
* means running the writer or reading the key — and a direct read of a `*.key`
|
|
19
|
+
* file is refused by the secret-read guard. A lock would need a human-presence
|
|
20
|
+
* primitive (a hardware key, an OS confirmation) this machine does not have.
|
|
21
|
+
*/
|
|
22
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import os from 'node:os';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
|
|
27
|
+
const VERSION = 'v1';
|
|
28
|
+
|
|
29
|
+
export function sealKeyPath(env = process.env) {
|
|
30
|
+
if (env.GREAT_CTO_SEAL_KEY) return env.GREAT_CTO_SEAL_KEY;
|
|
31
|
+
let home;
|
|
32
|
+
try { home = os.userInfo().homedir; } catch { home = os.homedir(); }
|
|
33
|
+
return path.join(home, '.great_cto', 'keys', 'verdict-seal.key');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The key, or null. `create` makes one (0600, in a 0700 directory) when there is none. */
|
|
37
|
+
export function loadSealKey({ create = false, env = process.env } = {}) {
|
|
38
|
+
const p = sealKeyPath(env);
|
|
39
|
+
try {
|
|
40
|
+
const hex = fs.readFileSync(p, 'utf8').trim();
|
|
41
|
+
if (/^[0-9a-f]{64}$/.test(hex)) return Buffer.from(hex, 'hex');
|
|
42
|
+
// A damaged key is not "no key": say so, every time, rather than unseal quietly.
|
|
43
|
+
process.stderr.write(`verdict-seal: ${p} is not a valid key — verdicts go unsealed until it is removed and recreated\n`);
|
|
44
|
+
return null;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
if (e.code !== 'ENOENT' || !create) return null;
|
|
47
|
+
}
|
|
48
|
+
fs.mkdirSync(path.dirname(p), { recursive: true, mode: 0o700 });
|
|
49
|
+
// Written whole to a temporary name and linked into place: two first-time writers,
|
|
50
|
+
// or a crash mid-write, must never leave an empty key that silently unseals
|
|
51
|
+
// every later verdict.
|
|
52
|
+
const tmp = `${p}.tmp-${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
53
|
+
fs.writeFileSync(tmp, randomBytes(32).toString('hex') + '\n', { flag: 'wx', mode: 0o600 });
|
|
54
|
+
try { fs.linkSync(tmp, p); }
|
|
55
|
+
catch (e) { if (e.code !== 'EEXIST') throw e; } // another writer made it first: use theirs
|
|
56
|
+
finally { fs.rmSync(tmp, { force: true }); }
|
|
57
|
+
return loadSealKey({ create: false, env });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Every field but the seal, keys sorted at every depth — so editing any of them
|
|
61
|
+
* (meta.need routes the pipeline, cost_usd feeds the spend view) breaks it. */
|
|
62
|
+
function canonical(v) {
|
|
63
|
+
if (Array.isArray(v)) return v.map(canonical);
|
|
64
|
+
if (v && typeof v === 'object') return Object.fromEntries(Object.keys(v).sort().filter((k) => k !== 'seal').map((k) => [k, canonical(v[k])]));
|
|
65
|
+
return v;
|
|
66
|
+
}
|
|
67
|
+
const VERDICT_FIELDS = (r) => ['verdict', canonical(r)];
|
|
68
|
+
const ACCEPT_FIELDS = (a) => ['accept', canonical(a)];
|
|
69
|
+
|
|
70
|
+
function mac(key, fields) {
|
|
71
|
+
return `${VERSION}:${createHmac('sha256', key).update(JSON.stringify(fields)).digest('hex')}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function check(key, fields, seal) {
|
|
75
|
+
if (typeof seal !== 'string' || !seal) return 'unsealed';
|
|
76
|
+
if (!key) return 'unknown';
|
|
77
|
+
const want = Buffer.from(mac(key, fields));
|
|
78
|
+
const got = Buffer.from(seal);
|
|
79
|
+
return want.length === got.length && timingSafeEqual(want, got) ? 'valid' : 'invalid';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function sealVerdict(rec, { key = loadSealKey({ create: true }) } = {}) {
|
|
83
|
+
return key ? mac(key, VERDICT_FIELDS(rec)) : null;
|
|
84
|
+
}
|
|
85
|
+
/** 'valid' | 'invalid' (written or edited outside the writer) | 'unsealed' (older, or never sealed) | 'unknown' (no key here). */
|
|
86
|
+
export function checkVerdictSeal(rec, { key = loadSealKey() } = {}) {
|
|
87
|
+
return check(key, VERDICT_FIELDS(rec || {}), rec?.seal);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function sealAcceptance(acc, { key = loadSealKey({ create: true }) } = {}) {
|
|
91
|
+
return key ? mac(key, ACCEPT_FIELDS(acc)) : null;
|
|
92
|
+
}
|
|
93
|
+
export function checkAcceptanceSeal(acc, { key = loadSealKey() } = {}) {
|
|
94
|
+
return check(key, ACCEPT_FIELDS(acc || {}), acc?.seal);
|
|
95
|
+
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { isMainModule } from './lib/is-main.mjs';
|
|
25
26
|
|
|
26
27
|
export const SIZE_BUDGET = 20 * 1024; // bytes: 20 KiB (20,480), not 20,000
|
|
27
28
|
export const DESC_MIN = 40;
|
|
@@ -162,7 +163,7 @@ export function lintSkills({ repoRoot }) {
|
|
|
162
163
|
return { skills: names.length, errors, warnings };
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
const isMain =
|
|
166
|
+
const isMain = isMainModule(import.meta.url);
|
|
166
167
|
if (isMain) {
|
|
167
168
|
const a = process.argv.slice(2);
|
|
168
169
|
const i = a.indexOf('--root');
|
package/package.json
CHANGED
package/postinstall.mjs
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* evidence of staleness.
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
-
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
37
|
+
import { readFileSync, readdirSync, existsSync, realpathSync } from 'node:fs';
|
|
38
38
|
import { spawn } from 'node:child_process';
|
|
39
39
|
import { join, dirname } from 'node:path';
|
|
40
40
|
import { homedir } from 'node:os';
|
|
@@ -174,6 +174,11 @@ export function ensureBoard({ env = process.env, spawnFn = spawn,
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
// Only when run as the hook, so the pure parts above stay importable by tests.
|
|
177
|
-
|
|
177
|
+
// Real paths on both sides, the rule of scripts/lib/is-main.mjs — inlined
|
|
178
|
+
// because the npm package does not ship scripts/.
|
|
179
|
+
const isMain = () => {
|
|
180
|
+
try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); } catch { return false; }
|
|
181
|
+
};
|
|
182
|
+
if (process.argv[1] && isMain()) {
|
|
178
183
|
try { main(); } catch { /* an installer must never fail the install */ }
|
|
179
184
|
}
|