codehere 1.0.1 → 1.0.2
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 +36 -192
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +110 -7
- package/dist/index.js +38 -1212
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
- package/dist/presentation/cli/commands/check-command.js +178 -23
- package/dist/presentation/cli/commands/help-command.js +21 -285
- package/dist/ui.js +5 -236
- package/dist/utils/version.js +0 -6
- package/package.json +9 -38
- package/dist/application/services/verdict-service.js +0 -154
- package/dist/application/use-cases/assess-cross-vendor.js +0 -126
- package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/export-aat.js +0 -107
- package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
- package/dist/application/use-cases/generate-final-report.js +0 -259
- package/dist/application/use-cases/generate-narration.js +0 -130
- package/dist/application/use-cases/generate-thread-title.js +0 -181
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/plan-auto-review.js +0 -57
- package/dist/application/use-cases/reconcile-audit.js +0 -100
- package/dist/application/use-cases/search-sessions.js +0 -70
- package/dist/application/use-cases/should-auto-review.js +0 -71
- package/dist/application/use-cases/thread-export.js +0 -245
- package/dist/application/use-cases/verify-claim.js +0 -456
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/audit.js +0 -11
- package/dist/domain/services/codebase-detector.js +0 -85
- package/dist/domain/task.js +0 -91
- package/dist/error-handler.js +0 -139
- package/dist/infrastructure/completion/bash-completion.js +0 -29
- package/dist/infrastructure/completion/fish-completion.js +0 -17
- package/dist/infrastructure/completion/zsh-completion.js +0 -25
- package/dist/infrastructure/config/config-manager.js +0 -174
- package/dist/infrastructure/config/product-context.js +0 -49
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -198
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/github-api.js +0 -110
- package/dist/infrastructure/context/github-device-flow.js +0 -65
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -122
- package/dist/infrastructure/context/registry-bootstrap.js +0 -95
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -114
- package/dist/infrastructure/cost/cost-router.js +0 -97
- package/dist/infrastructure/cost/spend-aggregator.js +0 -222
- package/dist/infrastructure/cost/team-ledger.js +0 -355
- package/dist/infrastructure/discovery/discovery-log.js +0 -85
- package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
- package/dist/infrastructure/hooks/hook-mode.js +0 -87
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
- package/dist/infrastructure/orchestration/context-handoff.js +0 -162
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -67
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
- package/dist/infrastructure/orchestration/graphify-context.js +0 -209
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1590
- package/dist/infrastructure/orchestration/session-persistence.js +0 -394
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
- package/dist/infrastructure/protocols/mcp-tools.js +0 -401
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
- package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
- package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
- package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
- package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
- package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
- package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
- package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
- package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
- package/dist/infrastructure/protocols/ui/styles.js +0 -2147
- package/dist/infrastructure/protocols/ui/tokens.js +0 -96
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
- package/dist/infrastructure/protocols/verify-page.js +0 -1677
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/fix-code.js +0 -144
- package/dist/infrastructure/security/fix-secret.js +0 -89
- package/dist/infrastructure/security/permissions-config.js +0 -235
- package/dist/infrastructure/security/scan-types.js +0 -10
- package/dist/infrastructure/security/ship-scan.js +0 -195
- package/dist/infrastructure/security/trust-scorer.js +0 -724
- package/dist/infrastructure/security/vibe-checks.js +0 -99
- package/dist/infrastructure/storage/atomic-write.js +0 -86
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
- package/dist/infrastructure/storage/project-thread-reader.js +0 -409
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -201
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -217
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -590
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -105
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -403
- package/dist/presentation/cli/commands/scan-command.js +0 -52
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
- package/dist/presentation/cli/commands/search-command.js +0 -51
- package/dist/presentation/cli/commands/setup-command.js +0 -330
- package/dist/presentation/cli/commands/status-command.js +0 -614
- package/dist/presentation/cli/commands/team-command.js +0 -256
- package/dist/presentation/cli/commands/thread-command.js +0 -370
- package/dist/presentation/cli/commands/trust-command.js +0 -328
- package/dist/presentation/cli/commands/verify-command.js +0 -298
- package/dist/presentation/cli/error-display.js +0 -328
- package/dist/presentation/cli/repl.js +0 -805
- package/dist/presentation/cli/task-deliverable.js +0 -153
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -147
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provable session search — REQUEST_METHOD stage 7 (Learn), and the Luca-seeded
|
|
3
|
-
* differentiator (#19): codehere already keeps a signed, cost-attributed record
|
|
4
|
-
* of every past session. This makes it SEARCHABLE — so a request can recover a
|
|
5
|
-
* prior decision, failed attempt, or command instead of starting from zero.
|
|
6
|
-
*
|
|
7
|
-
* The wedge vs a plain history search (e.g. ctx): every hit carries PROVENANCE —
|
|
8
|
-
* which agent did it and whether the run succeeded — because codehere's record
|
|
9
|
-
* has it and a raw-transcript search doesn't. "Search history you can trust."
|
|
10
|
-
*
|
|
11
|
-
* Local-first: reads the on-disk project thread records; no server, no network.
|
|
12
|
-
*/
|
|
13
|
-
import { listProjects, readProjectThread, } from '../../infrastructure/storage/project-thread-reader.js';
|
|
14
|
-
/** Pure ranking — score a task against query terms (prompt weighted 2x report). */
|
|
15
|
-
export function scoreTask(task, terms) {
|
|
16
|
-
const prompt = String(task.prompt || '').toLowerCase();
|
|
17
|
-
const report = String(task.finalReport?.summary || '').toLowerCase();
|
|
18
|
-
let score = 0;
|
|
19
|
-
for (const t of terms) {
|
|
20
|
-
if (prompt.includes(t))
|
|
21
|
-
score += 2;
|
|
22
|
-
if (report.includes(t))
|
|
23
|
-
score += 1;
|
|
24
|
-
}
|
|
25
|
-
let snippet = '';
|
|
26
|
-
const hay = report || prompt;
|
|
27
|
-
for (const t of terms) {
|
|
28
|
-
const i = hay.indexOf(t);
|
|
29
|
-
if (i >= 0) {
|
|
30
|
-
const start = Math.max(0, i - 30);
|
|
31
|
-
snippet = (start > 0 ? '…' : '') + hay.slice(start, start + 100).trim() + '…';
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return { score, snippet: snippet || String(task.prompt || '').slice(0, 90) };
|
|
36
|
-
}
|
|
37
|
-
/** Search past sessions across all projects. Ranked, top-N, with provenance. */
|
|
38
|
-
export function searchSessions(query, opts) {
|
|
39
|
-
const terms = String(query || '')
|
|
40
|
-
.toLowerCase()
|
|
41
|
-
.split(/\s+/)
|
|
42
|
-
.filter((w) => w.length >= 2);
|
|
43
|
-
if (!terms.length)
|
|
44
|
-
return [];
|
|
45
|
-
const limit = opts?.limit ?? 10;
|
|
46
|
-
const hits = [];
|
|
47
|
-
for (const p of listProjects()) {
|
|
48
|
-
const thread = readProjectThread(p.slug);
|
|
49
|
-
if (!thread)
|
|
50
|
-
continue;
|
|
51
|
-
for (const task of thread.tasks) {
|
|
52
|
-
if (task.archived)
|
|
53
|
-
continue;
|
|
54
|
-
const { score, snippet } = scoreTask(task, terms);
|
|
55
|
-
if (score > 0) {
|
|
56
|
-
hits.push({
|
|
57
|
-
sessionId: task.sessionId,
|
|
58
|
-
agent: task.agent || '?',
|
|
59
|
-
status: String(task.status || 'unknown'),
|
|
60
|
-
project: p.remote || p.slug,
|
|
61
|
-
prompt: task.prompt || '',
|
|
62
|
-
snippet,
|
|
63
|
-
score,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return hits.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
69
|
-
}
|
|
70
|
-
//# sourceMappingURL=search-sessions.js.map
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tier 2.1 · slice 1 — the pure decision: should a just-completed task get an
|
|
3
|
-
* AUTOMATIC cross-vendor adversarial review, and by which vendor?
|
|
4
|
-
*
|
|
5
|
-
* Today cross-vendor proof is opportunistic: it only fires if a human happened
|
|
6
|
-
* to run a second, different-vendor task that reviewed the first. 2.1 makes the
|
|
7
|
-
* differentiator automatic — on completion, codehere itself spawns a DIFFERENT
|
|
8
|
-
* vendor in refute-mode and folds its verdict into the composite (`SessionVerdict`).
|
|
9
|
-
*
|
|
10
|
-
* This module is ONLY the eligibility gate, kept pure so the policy is testable
|
|
11
|
-
* without a daemon, a spawn, or real spend (the spawn + verdict wiring are later
|
|
12
|
-
* slices). The honesty bar from assess-cross-vendor applies downstream: a real
|
|
13
|
-
* review with a real reviewer, never a laundered green.
|
|
14
|
-
*
|
|
15
|
-
* The three gates, in order:
|
|
16
|
-
* 1. cross-vendor possible — a DIFFERENT installed vendor exists to review with
|
|
17
|
-
* (single-vendor incumbents structurally can't do this; it's the whole wedge).
|
|
18
|
-
* 2. worth reviewing — the task actually changed something (no-op tasks have
|
|
19
|
-
* nothing to refute; reviewing them burns spend for a guaranteed-empty proof).
|
|
20
|
-
* 3. within budget — the review is a real spawn with real cost; never blow the
|
|
21
|
-
* cost-bounded promise to produce a receipt.
|
|
22
|
-
*/
|
|
23
|
-
/** Reviewer preference order — strongest-at-review first, mirrors ROUTING_RULES'
|
|
24
|
-
* review rule (claude-code leads, then codex). Any installed different-vendor
|
|
25
|
-
* agent is acceptable; this only decides WHICH when several are available. */
|
|
26
|
-
// Reviewer preference favors reliably-authed, cost-bounded vendors first: Claude +
|
|
27
|
-
// Cursor (flat subscriptions, ~free marginal) before the metered/rate-limit-prone
|
|
28
|
-
// ones (aider/opencode on the OpenAI key, codex on a ChatGPT plan, gemini needs a
|
|
29
|
-
// key). A runnable reviewer on a subscription you've paid for beats a "better" one
|
|
30
|
-
// that's out of quota.
|
|
31
|
-
const REVIEWER_PREFERENCE = ['claude-code', 'cursor', 'aider', 'opencode', 'codex', 'gemini'];
|
|
32
|
-
/** Pick the strongest available reviewer that is NOT the writer's vendor. */
|
|
33
|
-
function pickReviewer(writerAgent, installed) {
|
|
34
|
-
const candidates = installed.filter((a) => a !== writerAgent && a !== 'conductor');
|
|
35
|
-
if (candidates.length === 0)
|
|
36
|
-
return undefined;
|
|
37
|
-
for (const pref of REVIEWER_PREFERENCE) {
|
|
38
|
-
if (candidates.includes(pref))
|
|
39
|
-
return pref;
|
|
40
|
-
}
|
|
41
|
-
return candidates[0];
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Decide whether to auto-spawn a cross-vendor review. Pure; no I/O, no spend.
|
|
45
|
-
*/
|
|
46
|
-
export function shouldAutoReview(input, thresholds = {}) {
|
|
47
|
-
const minFiles = thresholds.minFilesChanged ?? 1;
|
|
48
|
-
const minBudget = thresholds.minBudgetUsd ?? 0.1;
|
|
49
|
-
const reviewerVendor = pickReviewer(input.writerAgent, input.installedAgents);
|
|
50
|
-
if (!reviewerVendor) {
|
|
51
|
-
return { eligible: false, reason: `No different vendor installed to review ${input.writerAgent}'s work.` };
|
|
52
|
-
}
|
|
53
|
-
if (input.filesChanged < minFiles) {
|
|
54
|
-
return {
|
|
55
|
-
eligible: false,
|
|
56
|
-
reason: `Nothing to review — the task changed ${input.filesChanged} file(s); a cross-vendor review needs real work to refute.`,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
if (input.budgetRemainingUsd < minBudget) {
|
|
60
|
-
return {
|
|
61
|
-
eligible: false,
|
|
62
|
-
reason: `Skipped to stay cost-bounded — $${input.budgetRemainingUsd.toFixed(2)} left under the cap, below the $${minBudget.toFixed(2)} a review needs.`,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
eligible: true,
|
|
67
|
-
reviewerVendor,
|
|
68
|
-
reason: `${reviewerVendor} will independently review ${input.writerAgent}'s work in refute-mode.`,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
//# sourceMappingURL=should-auto-review.js.map
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thread export · v0.8 S4 · signed-payload "GitHub for agent sessions"
|
|
3
|
-
*
|
|
4
|
-
* Produces a self-contained, Ed25519-signed bundle of one PersistedSession
|
|
5
|
-
* that a colleague can verify and replay locally. Local-first by design:
|
|
6
|
-
* never relayed through codehere's infrastructure · the user shares the
|
|
7
|
-
* file (or signed URL) by their preferred channel (Slack, email, PR
|
|
8
|
-
* description) and the recipient runs `codehere thread import <file>`.
|
|
9
|
-
*
|
|
10
|
-
* The signature covers a CANONICAL JSON form of the bundle body so two
|
|
11
|
-
* machines that re-serialize the same data get the same hash. Verification
|
|
12
|
-
* recomputes the canonical form, re-derives the hash, checks the signature
|
|
13
|
-
* against the included public key, and returns one of:
|
|
14
|
-
* - 'valid' · signature matches · safe to replay
|
|
15
|
-
* - 'tampered' · signature does NOT match · refuse
|
|
16
|
-
* - 'malformed' · bundle is structurally wrong · refuse
|
|
17
|
-
* - 'unknown_key' · signature valid but public key not in user's trust
|
|
18
|
-
* store · prompt user to trust+continue or refuse
|
|
19
|
-
*
|
|
20
|
-
* Pure logic. The CLI + HTTP routes wrap this and do I/O.
|
|
21
|
-
*/
|
|
22
|
-
import { sign as cryptoSign, verify as cryptoVerify, generateKeyPairSync, createPrivateKey, createPublicKey, } from 'crypto';
|
|
23
|
-
import { existsSync, readFileSync } from 'fs';
|
|
24
|
-
import { computeEntryHash, AUDIT_FILE } from '../../infrastructure/hooks/claude-sdk-hook.js';
|
|
25
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
26
|
-
/* Canonicalization · deterministic JSON form so signatures match */
|
|
27
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
28
|
-
/**
|
|
29
|
-
* Stable JSON: object keys sorted, no whitespace, primitives preserved.
|
|
30
|
-
* Two machines that build the same body MUST produce the same bytes for
|
|
31
|
-
* the signature to verify. We deliberately don't use JSON.stringify with
|
|
32
|
-
* a replacer because Node's serializer doesn't guarantee key order across
|
|
33
|
-
* versions/realms.
|
|
34
|
-
*/
|
|
35
|
-
export function canonicalize(value) {
|
|
36
|
-
if (value === null)
|
|
37
|
-
return 'null';
|
|
38
|
-
if (typeof value === 'number') {
|
|
39
|
-
if (!Number.isFinite(value))
|
|
40
|
-
throw new Error(`cannot canonicalize non-finite number: ${value}`);
|
|
41
|
-
return JSON.stringify(value);
|
|
42
|
-
}
|
|
43
|
-
if (typeof value === 'string' || typeof value === 'boolean')
|
|
44
|
-
return JSON.stringify(value);
|
|
45
|
-
if (Array.isArray(value)) {
|
|
46
|
-
return '[' + value.map(canonicalize).join(',') + ']';
|
|
47
|
-
}
|
|
48
|
-
if (typeof value === 'object') {
|
|
49
|
-
const obj = value;
|
|
50
|
-
const keys = Object.keys(obj).sort();
|
|
51
|
-
const parts = keys
|
|
52
|
-
.filter((k) => obj[k] !== undefined) // undefined → drop the key (JSON convention)
|
|
53
|
-
.map((k) => JSON.stringify(k) + ':' + canonicalize(obj[k]));
|
|
54
|
-
return '{' + parts.join(',') + '}';
|
|
55
|
-
}
|
|
56
|
-
if (value === undefined)
|
|
57
|
-
return 'null';
|
|
58
|
-
throw new Error(`cannot canonicalize type: ${typeof value}`);
|
|
59
|
-
}
|
|
60
|
-
/** Generate a fresh Ed25519 keypair · used for first-time setup. */
|
|
61
|
-
export function generateKeypair() {
|
|
62
|
-
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
63
|
-
return { publicKey, privateKey };
|
|
64
|
-
}
|
|
65
|
-
/** Sign a bundle body and produce the full ThreadBundle.
|
|
66
|
-
* Ed25519 is NOT a hash-then-sign scheme · pass the message directly to
|
|
67
|
-
* crypto.sign(null, msg, key) · the algorithm is implied by the key. */
|
|
68
|
-
export function signBundle(body, keys) {
|
|
69
|
-
const canonical = canonicalize(body);
|
|
70
|
-
const sig = cryptoSign(null, Buffer.from(canonical, 'utf-8'), keys.privateKey);
|
|
71
|
-
const pubRaw = keys.publicKey.export({ format: 'der', type: 'spki' });
|
|
72
|
-
return {
|
|
73
|
-
body,
|
|
74
|
-
signature: sig.toString('base64'),
|
|
75
|
-
publicKey: pubRaw.toString('base64'),
|
|
76
|
-
alg: 'ed25519',
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Verify a bundle. Returns one of:
|
|
81
|
-
* - valid · signature matches AND the publicKey is in trustedKeys
|
|
82
|
-
* - valid-untrusted · signature matches but the key isn't in trustedKeys
|
|
83
|
-
* (returned with status='valid' but trusted=false)
|
|
84
|
-
* - tampered · signature doesn't match the recomputed body bytes
|
|
85
|
-
* - malformed · bundle structure broken
|
|
86
|
-
*/
|
|
87
|
-
export function verifyBundle(bundle, trustedKeys = new Set()) {
|
|
88
|
-
if (!isBundle(bundle))
|
|
89
|
-
return { status: 'malformed', reason: 'bundle shape is invalid' };
|
|
90
|
-
if (bundle.alg !== 'ed25519')
|
|
91
|
-
return { status: 'malformed', reason: `unsupported alg: ${bundle.alg}` };
|
|
92
|
-
try {
|
|
93
|
-
const canonical = canonicalize(bundle.body);
|
|
94
|
-
const pubBuf = Buffer.from(bundle.publicKey, 'base64');
|
|
95
|
-
const sigBuf = Buffer.from(bundle.signature, 'base64');
|
|
96
|
-
const pubKey = createPublicKey({ key: pubBuf, format: 'der', type: 'spki' });
|
|
97
|
-
const ok = cryptoVerify(null, Buffer.from(canonical, 'utf-8'), pubKey, sigBuf);
|
|
98
|
-
if (!ok)
|
|
99
|
-
return { status: 'tampered', reason: 'signature does not match body' };
|
|
100
|
-
const trusted = trustedKeys.has(bundle.publicKey);
|
|
101
|
-
if (!trusted) {
|
|
102
|
-
return { status: 'unknown_key', bundle, publicKey: bundle.publicKey };
|
|
103
|
-
}
|
|
104
|
-
return { status: 'valid', bundle, trusted: true };
|
|
105
|
-
}
|
|
106
|
-
catch (err) {
|
|
107
|
-
return { status: 'malformed', reason: err instanceof Error ? err.message : String(err) };
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
function isBundle(x) {
|
|
111
|
-
if (typeof x !== 'object' || x === null)
|
|
112
|
-
return false;
|
|
113
|
-
const obj = x;
|
|
114
|
-
if (typeof obj.signature !== 'string')
|
|
115
|
-
return false;
|
|
116
|
-
if (typeof obj.publicKey !== 'string')
|
|
117
|
-
return false;
|
|
118
|
-
if (typeof obj.alg !== 'string')
|
|
119
|
-
return false;
|
|
120
|
-
const body = obj.body;
|
|
121
|
-
if (typeof body !== 'object' || body === null)
|
|
122
|
-
return false;
|
|
123
|
-
const b = body;
|
|
124
|
-
if (b.schema !== 1)
|
|
125
|
-
return false;
|
|
126
|
-
if (b.kind !== 'thread')
|
|
127
|
-
return false;
|
|
128
|
-
if (typeof b.sessionId !== 'string')
|
|
129
|
-
return false;
|
|
130
|
-
if (typeof b.session !== 'object' || b.session === null)
|
|
131
|
-
return false;
|
|
132
|
-
// auditChain is optional, but if present it must be an array (the signature
|
|
133
|
-
// already protects its contents; this just guards the shape).
|
|
134
|
-
if (b.auditChain !== undefined && !Array.isArray(b.auditChain))
|
|
135
|
-
return false;
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
139
|
-
/* Audit chain · embed (export side) + re-verify (import side) */
|
|
140
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
141
|
-
/**
|
|
142
|
-
* Collect the audit entries for one session from the on-disk chain, preserving
|
|
143
|
-
* each entry's original chain fields (seq/prevHash/entryHash). The export use-case
|
|
144
|
-
* puts these on ThreadBundleBody.auditChain so the recipient can re-verify the
|
|
145
|
-
* agent's actions. Best-effort: a missing/unreadable log yields [].
|
|
146
|
-
*/
|
|
147
|
-
export function collectAuditEntriesForSession(sessionId, filepath = AUDIT_FILE, alsoSessionIds = []) {
|
|
148
|
-
if (!existsSync(filepath))
|
|
149
|
-
return [];
|
|
150
|
-
let content;
|
|
151
|
-
try {
|
|
152
|
-
content = readFileSync(filepath, 'utf-8');
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
return [];
|
|
156
|
-
}
|
|
157
|
-
// 1.3 (extended to audit reconciliation) · a conductor-routed task's tool
|
|
158
|
-
// calls are keyed by the SPECIALIST's runtime sid, not the conductor task id.
|
|
159
|
-
// Matching the delegate id too lets reconciliation see the real writes —
|
|
160
|
-
// without it Auto tasks reconcile 'no-writes' and never earn the observed
|
|
161
|
-
// signal, so the 2.2 composite grade stays dark for the default path.
|
|
162
|
-
const ids = new Set([sessionId, ...alsoSessionIds]);
|
|
163
|
-
const out = [];
|
|
164
|
-
for (const line of content.split('\n')) {
|
|
165
|
-
if (!line.trim())
|
|
166
|
-
continue;
|
|
167
|
-
try {
|
|
168
|
-
const e = JSON.parse(line);
|
|
169
|
-
if (ids.has(e.sessionId))
|
|
170
|
-
out.push(e);
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
/* skip unparseable line */
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return out;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Re-verify an embedded audit chain's PER-ENTRY integrity: each chained entry's
|
|
180
|
-
* stored entryHash must equal its recomputed hash (computeEntryHash, the exact
|
|
181
|
-
* function the writer used). A mismatch means the entry's content was altered
|
|
182
|
-
* after it was written → the record of what the agent did is not trustworthy.
|
|
183
|
-
*
|
|
184
|
-
* This is deliberately a self-hash check, not a cross-link walk: a single-session
|
|
185
|
-
* export is a SUBSET of the global interleaved chain, so its entries' prevHash
|
|
186
|
-
* values point at other sessions' entries that aren't present. Per-entry self-hash
|
|
187
|
-
* + the bundle signature (signer vouches for the exact set) is the honest,
|
|
188
|
-
* verifiable team-proof; global completeness is not claimable from a subset. See
|
|
189
|
-
* ThreadBundleBody.auditChain for the FULL trust model (signer attestation, not
|
|
190
|
-
* independent provenance — a malicious signer can fabricate a valid chain).
|
|
191
|
-
*
|
|
192
|
-
* CALLER CONTRACT: `valid` means "no tampered entry was found" — it is `true`
|
|
193
|
-
* for an EMPTY or all-legacy set where NOTHING was cryptographically checked.
|
|
194
|
-
* Any "✓ verified" surface MUST gate on `verified > 0`, not bare `valid`, or it
|
|
195
|
-
* will claim verification that didn't happen.
|
|
196
|
-
*/
|
|
197
|
-
export function verifyEmbeddedAuditChain(entries) {
|
|
198
|
-
const check = { valid: true, total: 0, verified: 0, legacy: 0, tampered: [] };
|
|
199
|
-
for (const entry of entries) {
|
|
200
|
-
check.total++;
|
|
201
|
-
const hasChain = typeof entry.seq === 'number' && typeof entry.prevHash === 'string' && typeof entry.entryHash === 'string';
|
|
202
|
-
if (!hasChain) {
|
|
203
|
-
check.legacy++;
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
const recomputed = computeEntryHash(entry);
|
|
207
|
-
if (recomputed !== entry.entryHash) {
|
|
208
|
-
check.valid = false;
|
|
209
|
-
check.tampered.push({
|
|
210
|
-
seq: entry.seq,
|
|
211
|
-
sessionId: entry.sessionId,
|
|
212
|
-
toolName: entry.toolName,
|
|
213
|
-
reason: 'self_hash_mismatch',
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
check.verified++;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return check;
|
|
221
|
-
}
|
|
222
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
223
|
-
/* Key derivation helpers · PEM encoding for storage */
|
|
224
|
-
/* ──────────────────────────────────────────────────────────────────────── */
|
|
225
|
-
export function exportPrivatePem(key) {
|
|
226
|
-
return key.export({ format: 'pem', type: 'pkcs8' }).toString();
|
|
227
|
-
}
|
|
228
|
-
export function exportPublicPem(key) {
|
|
229
|
-
return key.export({ format: 'pem', type: 'spki' }).toString();
|
|
230
|
-
}
|
|
231
|
-
export function importPrivatePem(pem) {
|
|
232
|
-
return createPrivateKey({ key: pem, format: 'pem', type: 'pkcs8' });
|
|
233
|
-
}
|
|
234
|
-
export function importPublicPem(pem) {
|
|
235
|
-
return createPublicKey({ key: pem, format: 'pem', type: 'spki' });
|
|
236
|
-
}
|
|
237
|
-
/** Compute the base64 publicKey string for a KeyObject · same encoding as in bundles. */
|
|
238
|
-
export function publicKeyToBase64(key) {
|
|
239
|
-
return key.export({ format: 'der', type: 'spki' }).toString('base64');
|
|
240
|
-
}
|
|
241
|
-
/** Short fingerprint used in human-readable output. */
|
|
242
|
-
export function shortFingerprint(publicKeyB64) {
|
|
243
|
-
return publicKeyB64.slice(0, 8) + '...' + publicKeyB64.slice(-4);
|
|
244
|
-
}
|
|
245
|
-
//# sourceMappingURL=thread-export.js.map
|