codehere 0.7.1 → 1.0.1
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 +22 -13
- package/dist/application/services/verdict-service.js +154 -0
- package/dist/application/use-cases/assess-cross-vendor.js +126 -0
- package/dist/application/use-cases/build-cross-vendor-verdict.js +118 -0
- package/dist/application/use-cases/export-aat.js +107 -0
- package/dist/application/use-cases/generate-acceptance-criteria.js +223 -0
- package/dist/application/use-cases/generate-final-report.js +140 -15
- package/dist/application/use-cases/generate-narration.js +5 -3
- package/dist/application/use-cases/generate-thread-title.js +5 -2
- package/dist/application/use-cases/narrator-prompt-template.js +1 -1
- package/dist/application/use-cases/plan-auto-review.js +57 -0
- package/dist/application/use-cases/reconcile-audit.js +100 -0
- package/dist/application/use-cases/rerun-tests.js +226 -0
- package/dist/application/use-cases/search-sessions.js +70 -0
- package/dist/application/use-cases/should-auto-review.js +71 -0
- package/dist/application/use-cases/thread-export.js +90 -0
- package/dist/application/use-cases/verify-claim.js +187 -5
- package/dist/domain/audit.js +11 -0
- package/dist/domain/services/codebase-detector.js +0 -6
- package/dist/domain/task.js +91 -0
- package/dist/error-handler.js +3 -115
- package/dist/index.js +279 -31
- package/dist/infrastructure/completion/bash-completion.js +7 -3
- package/dist/infrastructure/completion/fish-completion.js +7 -12
- package/dist/infrastructure/completion/zsh-completion.js +7 -12
- package/dist/infrastructure/config/config-manager.js +4 -3
- package/dist/infrastructure/config/product-context.js +1 -19
- package/dist/infrastructure/context/adapters/figma-source.js +37 -2
- package/dist/infrastructure/context/adapters/github-issue-source.js +3 -2
- package/dist/infrastructure/context/github-api.js +110 -0
- package/dist/infrastructure/context/github-device-flow.js +65 -0
- package/dist/infrastructure/context/oauth-store.js +5 -2
- package/dist/infrastructure/context/registry-bootstrap.js +0 -4
- package/dist/infrastructure/cost/budget-config.js +4 -3
- package/dist/infrastructure/cost/cost-router.js +16 -23
- package/dist/infrastructure/cost/spend-aggregator.js +91 -16
- package/dist/infrastructure/cost/team-ledger.js +355 -0
- package/dist/infrastructure/discovery/discovery-log.js +85 -0
- package/dist/infrastructure/hooks/audit-tip-signature.js +336 -0
- package/dist/infrastructure/hooks/claude-sdk-hook.js +139 -3
- package/dist/infrastructure/hooks/hook-mode.js +20 -4
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +8 -2
- package/dist/infrastructure/hooks/pre-prompt-scan.js +3 -3
- package/dist/infrastructure/observability/execution-tracer.js +1 -1
- package/dist/infrastructure/orchestration/agents-config-seed.js +23 -11
- package/dist/infrastructure/orchestration/aider-adapter.js +188 -24
- package/dist/infrastructure/orchestration/artifacts-extractor.js +60 -2
- package/dist/infrastructure/orchestration/cassette-recorder.js +1 -1
- package/dist/infrastructure/orchestration/claude-code-adapter.js +117 -3
- package/dist/infrastructure/orchestration/codex-adapter.js +52 -16
- package/dist/infrastructure/orchestration/conductor-adapter.js +46 -7
- package/dist/infrastructure/orchestration/context-handoff.js +32 -14
- package/dist/infrastructure/orchestration/detect-ollama.js +75 -0
- package/dist/infrastructure/orchestration/failover-config.js +1 -11
- package/dist/infrastructure/orchestration/gemini-adapter.js +92 -12
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +134 -3
- package/dist/infrastructure/orchestration/graphify-context.js +14 -59
- package/dist/infrastructure/orchestration/memory-injector.js +4 -1
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +167 -9
- package/dist/infrastructure/orchestration/opencode-adapter.js +84 -14
- package/dist/infrastructure/orchestration/progress-aggregator.js +275 -23
- package/dist/infrastructure/orchestration/session-manager.js +593 -94
- package/dist/infrastructure/orchestration/session-persistence.js +177 -41
- package/dist/infrastructure/orchestration/thread-key-store.js +1 -1
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -8
- package/dist/infrastructure/protocols/cloud-sync.js +2 -2
- package/dist/infrastructure/protocols/mcp-helpers.js +1 -1
- package/dist/infrastructure/protocols/mcp-server-impl.js +1498 -354
- package/dist/infrastructure/protocols/mcp-tools.js +401 -0
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +121 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +44 -0
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +56 -0
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +588 -0
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +168 -0
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +67 -0
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +115 -0
- package/dist/infrastructure/protocols/ui/app-script-failover.js +133 -0
- package/dist/infrastructure/protocols/ui/app-script-github.js +113 -0
- package/dist/infrastructure/protocols/ui/app-script-observer.js +104 -0
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +80 -0
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +203 -0
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +136 -0
- package/dist/infrastructure/protocols/ui/app-script-render.js +166 -0
- package/dist/infrastructure/protocols/ui/app-script-share.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +194 -0
- package/dist/infrastructure/protocols/ui/app-script-tour.js +78 -0
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +312 -0
- package/dist/infrastructure/protocols/ui/app-script.js +2998 -0
- package/dist/infrastructure/protocols/ui/font-faces.js +7 -0
- package/dist/infrastructure/protocols/ui/styles.js +2147 -0
- package/dist/infrastructure/protocols/ui/tokens.js +96 -0
- package/dist/infrastructure/protocols/ui-page-v2.js +116 -4126
- package/dist/infrastructure/protocols/verify-page.js +884 -148
- package/dist/infrastructure/security/fix-code.js +144 -0
- package/dist/infrastructure/security/fix-secret.js +89 -0
- package/dist/infrastructure/security/permissions-config.js +3 -2
- package/dist/infrastructure/security/scan-types.js +10 -0
- package/dist/infrastructure/security/ship-scan.js +195 -0
- package/dist/infrastructure/security/trust-scorer.js +70 -3
- package/dist/infrastructure/security/vibe-checks.js +99 -0
- package/dist/infrastructure/storage/atomic-write.js +86 -0
- package/dist/infrastructure/storage/project-thread-mirror.js +151 -4
- package/dist/infrastructure/storage/project-thread-reader.js +262 -25
- package/dist/infrastructure/storage/project-thread-title-backfill.js +1 -1
- package/dist/monitoring.js +2 -2
- package/dist/presentation/cli/commands/audit-command.js +79 -4
- package/dist/presentation/cli/commands/budget-command.js +2 -2
- package/dist/presentation/cli/commands/check-command.js +96 -0
- package/dist/presentation/cli/commands/context-source-command.js +20 -1
- package/dist/presentation/cli/commands/help-command.js +5 -5
- package/dist/presentation/cli/commands/hook-command.js +5 -5
- package/dist/presentation/cli/commands/init-command.js +89 -66
- package/dist/presentation/cli/commands/memory-command.js +28 -28
- package/dist/presentation/cli/commands/permission-command.js +1 -12
- package/dist/presentation/cli/commands/protocols-builder.js +2 -2
- package/dist/presentation/cli/commands/protocols-command.js +4 -4
- package/dist/presentation/cli/commands/repo-map-builder.js +1 -1
- package/dist/presentation/cli/commands/report-command.js +48 -32
- package/dist/presentation/cli/commands/scan-command.js +52 -0
- package/dist/presentation/cli/commands/scan-tool-command.js +24 -8
- package/dist/presentation/cli/commands/search-command.js +51 -0
- package/dist/presentation/cli/commands/setup-command.js +31 -8
- package/dist/presentation/cli/commands/status-command.js +39 -86
- package/dist/presentation/cli/commands/team-command.js +256 -0
- package/dist/presentation/cli/commands/thread-command.js +87 -5
- package/dist/presentation/cli/commands/trust-command.js +53 -31
- package/dist/presentation/cli/commands/verify-command.js +74 -9
- package/dist/presentation/cli/error-display.js +0 -8
- package/dist/presentation/cli/repl.js +15 -7
- package/dist/presentation/cli/task-deliverable.js +36 -10
- package/dist/testing/replay-adapter.js +0 -19
- package/dist/ui.js +51 -239
- package/dist/utils/version.js +0 -6
- package/package.json +30 -10
- package/dist/domain/entities/task.js +0 -61
- package/dist/infrastructure/protocols/ui-page.js +0 -2377
- package/dist/infrastructure/security/input-validator.js +0 -152
- package/dist/infrastructure/storage/embeddings-path.js +0 -37
- package/dist/infrastructure/storage/task-repository.js +0 -206
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// MCP tool layer extracted from mcp-server-impl.ts (god-file split, 2026-06-29).
|
|
2
|
+
// The JSON-RPC surface: tool definitions + the read-only handlers + the dispatcher.
|
|
3
|
+
// The HTTP daemon (startMCPServer) stays in mcp-server-impl.ts and imports dispatch+TOOLS.
|
|
4
|
+
import { existsSync, readFileSync } from 'fs';
|
|
5
|
+
import { relative, resolve as resolvePath, sep } from 'path';
|
|
6
|
+
import { computeSpendToday } from '../cost/spend-aggregator.js';
|
|
7
|
+
import { loadBudget, budgetUsedPct } from '../cost/budget-config.js';
|
|
8
|
+
import { loadTeamConfig, writeMyShare, buildTeamView } from '../cost/team-ledger.js';
|
|
9
|
+
import { collectFilesForMCP, scoreFileForMCP, aggregateTrust } from './mcp-helpers.js';
|
|
10
|
+
import { scanPayload, calculateRiskScore, AUDIT_FILE, verifyAuditChain } from '../hooks/claude-sdk-hook.js';
|
|
11
|
+
import { getVersion } from '../../utils/version.js';
|
|
12
|
+
// ── Tool definitions ─────────────────────────────────────────────────────────
|
|
13
|
+
// Tool annotations follow the MCP spec (the github/github-mcp-server pattern:
|
|
14
|
+
// toolsets + an explicit read-only declaration). Every codehere MCP tool is
|
|
15
|
+
// ANALYSIS-ONLY — none writes files, spawns agents, or mutates state — and the
|
|
16
|
+
// annotations SAY so at the protocol level (readOnlyHint), so a cautious client
|
|
17
|
+
// can mount codehere's MCP knowing the whole surface is inspect-only. The
|
|
18
|
+
// 'toolset' grouping mirrors gh-mcp's toolset concept (security · audit).
|
|
19
|
+
// NOTE: readOnlyHint/title are MCP-spec (rev 2025-03-26) fields — we still
|
|
20
|
+
// declare protocolVersion 2024-11-05, where clients ignore unknown fields, so
|
|
21
|
+
// this is additive; 'toolset' is a CUSTOM extension (gh-mcp's toolsets are a
|
|
22
|
+
// server-side flag, not a wire field) and annotations are self-declarations,
|
|
23
|
+
// not proofs. If a WRITE tool is ever added, it must carry readOnlyHint:false
|
|
24
|
+
// + its own destructiveHint — and the README line "Exposes 4 read-only tools"
|
|
25
|
+
// (README.md §MCP) must change in the same commit (binding honesty rule).
|
|
26
|
+
export const TOOLS = [
|
|
27
|
+
{
|
|
28
|
+
name: 'codehere_trust',
|
|
29
|
+
annotations: { title: 'Trust-score a file or snippet', readOnlyHint: true, toolset: 'security' },
|
|
30
|
+
description: 'Scan a file or code snippet for security vulnerabilities. Returns a trust score 0-100, grade, and plain-English findings. Zero API calls — runs entirely locally.',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
filepath: { type: 'string', description: 'Path to file (relative to cwd)' },
|
|
35
|
+
code: { type: 'string', description: 'Code content to scan (alternative to filepath)' },
|
|
36
|
+
verbose: { type: 'boolean', description: 'Include all findings with line numbers' },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'codehere_scan',
|
|
42
|
+
annotations: { title: 'Scan text for prompt-injection / threats', readOnlyHint: true, toolset: 'security' },
|
|
43
|
+
description: 'Scan an instruction, prompt, or command text for injection attacks, credential theft attempts, destructive operations, and other threats. Returns block/allow decision with findings.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
required: ['text'],
|
|
47
|
+
properties: {
|
|
48
|
+
text: { type: 'string', description: 'Text to scan (instruction, prompt, command)' },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'codehere_review',
|
|
54
|
+
annotations: { title: 'Review a directory, rank riskiest files', readOnlyHint: true, toolset: 'security' },
|
|
55
|
+
description: 'Review all files in a directory and return trust scores. Identifies the riskiest files to focus review effort.',
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
path: { type: 'string', description: 'Directory path (default: current directory)' },
|
|
60
|
+
topN: { type: 'number', description: 'Return top N riskiest files (default: 10)' },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'codehere_audit',
|
|
66
|
+
annotations: { title: 'Read / verify the hash-chained audit log', readOnlyHint: true, toolset: 'audit' },
|
|
67
|
+
description: 'Read recent hook audit log entries — shows which tool calls codehere blocked, allowed, or overrode (fail-open paths) in Claude Code sessions. Pass verify:true to instead validate the SHA-256 hash chain and return any tampering detected.',
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
limit: { type: 'number', description: 'Number of recent entries to return (default: 20)' },
|
|
72
|
+
blockedOnly: { type: 'boolean', description: 'Only return blocked events' },
|
|
73
|
+
overrideOnly: {
|
|
74
|
+
type: 'boolean',
|
|
75
|
+
description: 'Only return trust-override events — cases where the scanner was bypassed (stdin failure, parse failure, --log-only, scan exception). Per CC frontier Q1 (auditable permission trails) — surface every bypass for review.',
|
|
76
|
+
},
|
|
77
|
+
verify: {
|
|
78
|
+
type: 'boolean',
|
|
79
|
+
description: 'Verify the SHA-256 hash chain instead of returning entries. Returns { valid, totalEntries, chainedEntries, legacyEntries, errors[] } where errors lists every detected tampering point (modification / deletion / insertion / parse failure). Per CC frontier Q1 (cryptographic commitment + tamper evidence).',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'codehere_spend',
|
|
86
|
+
annotations: { title: "Today's estimated spend by agent and project", readOnlyHint: true, toolset: 'cost' },
|
|
87
|
+
description: "Today's estimated coding-agent spend on this machine, broken down by agent and project, against the daily budget. So you can answer 'what have we spent today' from inside the session. Estimate from real token counts at published rates, not a provider invoice.",
|
|
88
|
+
inputSchema: { type: 'object', properties: {} },
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'codehere_team',
|
|
92
|
+
annotations: { title: 'Merged team cost + record across machines', readOnlyHint: true, toolset: 'cost' },
|
|
93
|
+
description: "The team's merged cost and integrity-checked record across every machine that shares the team git repo (spend by machine, total recorded actions, any broken audit chains). Falls back to this machine's solo view when no team is configured. Pass strict:true to REFUSE a total when any share is tampered or its audit chain is broken, instead of reporting a number that can't be trusted.",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
strict: {
|
|
98
|
+
type: 'boolean',
|
|
99
|
+
description: 'When true, if any share is tampered or its audit chain is broken, return a refusal naming the untrustworthy machines instead of a team total.',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
/** codehere_spend MCP tool: today's estimated spend, structured numbers plus a
|
|
106
|
+
* one-line summary the agent can quote. Reads local state only. */
|
|
107
|
+
function handleSpend(_params) {
|
|
108
|
+
const s = computeSpendToday();
|
|
109
|
+
const cfg = loadBudget();
|
|
110
|
+
const fmt = (n) => '$' + (Number(n) || 0).toFixed(2);
|
|
111
|
+
const pct = budgetUsedPct(s.totalUsd, cfg);
|
|
112
|
+
const head = cfg.dailyUsd != null
|
|
113
|
+
? `codehere spend today: ${fmt(s.totalUsd)} of ${fmt(cfg.dailyUsd)} daily budget (${pct ?? 0}% used), across ${s.sessionCount} session${s.sessionCount === 1 ? '' : 's'}.`
|
|
114
|
+
: `codehere spend today: ${fmt(s.totalUsd)} across ${s.sessionCount} session${s.sessionCount === 1 ? '' : 's'}, no budget set.`;
|
|
115
|
+
const agents = Object.entries(s.byAgent || {})
|
|
116
|
+
.map(([a, v]) => ` ${a}: ${fmt(v.usd)} (${v.sessions} session${v.sessions === 1 ? '' : 's'})`)
|
|
117
|
+
.join('\n') || ' (no agent spend today)';
|
|
118
|
+
const projects = Object.entries(s.byProject || {})
|
|
119
|
+
.map(([p, v]) => ` ${p}: ${fmt(v.usd)}`)
|
|
120
|
+
.join('\n') || ' (no project spend today)';
|
|
121
|
+
const raw = JSON.stringify({
|
|
122
|
+
totalUsd: s.totalUsd,
|
|
123
|
+
sessionCount: s.sessionCount,
|
|
124
|
+
dailyBudgetUsd: cfg.dailyUsd,
|
|
125
|
+
pctUsed: pct,
|
|
126
|
+
byAgent: s.byAgent,
|
|
127
|
+
byProject: s.byProject,
|
|
128
|
+
tokens: s.tokens,
|
|
129
|
+
});
|
|
130
|
+
return `${head}\nEstimate from real token counts at published rates, not a provider invoice.\n\nBy agent:\n${agents}\n\nBy project:\n${projects}\n\nRaw: ${raw}`;
|
|
131
|
+
}
|
|
132
|
+
/** codehere_team MCP tool: the merged team view, or this machine's solo view when
|
|
133
|
+
* no team git repo is configured. Reads local + the team dir only. */
|
|
134
|
+
function handleTeam(_params) {
|
|
135
|
+
const strict = Boolean(_params?.strict);
|
|
136
|
+
const fmt = (n) => '$' + (Number(n) || 0).toFixed(2);
|
|
137
|
+
const cfg = loadTeamConfig();
|
|
138
|
+
if (!cfg.dir) {
|
|
139
|
+
const s = computeSpendToday();
|
|
140
|
+
return `No team configured. This machine today: ${fmt(s.totalUsd)} across ${s.sessionCount} session${s.sessionCount === 1 ? '' : 's'}. Run "codehere team set-dir <a git repo>" on each machine to merge the whole team, local-first.`;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
writeMyShare(cfg.dir);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
/* dir not writable; still read what is there */
|
|
147
|
+
}
|
|
148
|
+
const v = buildTeamView(cfg.dir, cfg.dailyUsd);
|
|
149
|
+
const count = (v.machines || []).length;
|
|
150
|
+
const machines = (v.machines || []).map((m) => ` ${m.machineId}: ${fmt(m.totalUsd)}`).join('\n') || ' (none)';
|
|
151
|
+
const agents = Object.entries(v.perAgent || {}).sort((a, b) => b[1] - a[1]);
|
|
152
|
+
const byAgent = agents.length
|
|
153
|
+
? '\n\nBy agent (cost · what each did):\n' +
|
|
154
|
+
agents
|
|
155
|
+
.map(([a, usd]) => {
|
|
156
|
+
const s = v.perAgentSessions?.[a] ?? 0;
|
|
157
|
+
return ` ${a}: ${fmt(usd)} · ${s} session${s === 1 ? '' : 's'}`;
|
|
158
|
+
})
|
|
159
|
+
.join('\n')
|
|
160
|
+
: '';
|
|
161
|
+
const broken = v.brokenChains || [];
|
|
162
|
+
const brokenLine = broken.length
|
|
163
|
+
? `\nBROKEN audit chain on: ${broken.join(', ')}. Investigate before trusting that record.`
|
|
164
|
+
: '';
|
|
165
|
+
const tampered = v.tamperedShares || [];
|
|
166
|
+
const unsigned = v.unsignedShares || [];
|
|
167
|
+
const sigLine = tampered.length
|
|
168
|
+
? `\nSIGNATURE INVALID on: ${tampered.join(', ')} — that share was altered or re-signed with a different key than first seen. Do not trust it.`
|
|
169
|
+
: count
|
|
170
|
+
? `\nAll shares signed; keys match the ones first seen (no post-write change detected).`
|
|
171
|
+
: '';
|
|
172
|
+
const unsignedLine = unsigned.length ? `\nUnsigned, can't verify: ${unsigned.join(', ')}.` : '';
|
|
173
|
+
if (strict) {
|
|
174
|
+
const bad = [...new Set([...tampered, ...broken])];
|
|
175
|
+
if (bad.length) {
|
|
176
|
+
return `REFUSED (strict): the team record can't be trusted — ${bad.join(', ')} ${bad.length === 1 ? 'is' : 'are'} tampered or ${bad.length === 1 ? 'has' : 'have'} a broken audit chain. Not reporting a total. Investigate that machine's share before trusting the record.\n\nRaw: ${JSON.stringify({ strict: true, ok: false, untrustworthy: bad, tamperedShares: tampered, brokenChains: broken })}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const head = `codehere team today: ${fmt(v.teamTotal)}${v.dailyUsd != null ? ` of ${fmt(v.dailyUsd)} team budget` : ''} across ${count} machine${count === 1 ? '' : 's'}, ${(v.totalActions || 0).toLocaleString()} recorded actions.`;
|
|
180
|
+
const practiceLine = v.practice && v.practice.sessions > 0
|
|
181
|
+
? `\nPractice runs: ${v.practice.sessions} session${v.practice.sessions === 1 ? '' : 's'} · ${fmt(v.practice.usd)} fabricated · not in the totals.`
|
|
182
|
+
: '';
|
|
183
|
+
return `${head}\nEstimate from real tokens at published rates, not a provider invoice.${practiceLine}${brokenLine}${sigLine}${unsignedLine}\n\nBy machine:\n${machines}${byAgent}\n\nRaw: ${JSON.stringify(v)}`;
|
|
184
|
+
}
|
|
185
|
+
function handleTrust(params) {
|
|
186
|
+
const { filepath, code, verbose } = params;
|
|
187
|
+
let content;
|
|
188
|
+
let label;
|
|
189
|
+
if (code) {
|
|
190
|
+
content = code;
|
|
191
|
+
label = 'provided code';
|
|
192
|
+
}
|
|
193
|
+
else if (filepath) {
|
|
194
|
+
// MED #31 (atomic audit) · scope the inspect-only trust tool to the cwd:
|
|
195
|
+
// '../../etc/passwd' walked out of the workspace. Read-only, so the
|
|
196
|
+
// blast radius was disclosure-shaped — still, the tool's contract says
|
|
197
|
+
// "scores YOUR project's file".
|
|
198
|
+
const resolved = resolvePath(process.cwd(), filepath);
|
|
199
|
+
if (resolved !== process.cwd() && !resolved.startsWith(process.cwd() + sep)) {
|
|
200
|
+
return JSON.stringify({ error: `Path escapes the workspace: ${filepath}` });
|
|
201
|
+
}
|
|
202
|
+
if (!existsSync(resolved)) {
|
|
203
|
+
return JSON.stringify({ error: `File not found: ${filepath}` });
|
|
204
|
+
}
|
|
205
|
+
content = readFileSync(resolved, 'utf-8');
|
|
206
|
+
label = filepath;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
return JSON.stringify({ error: 'Provide either filepath or code' });
|
|
210
|
+
}
|
|
211
|
+
const result = scoreFileForMCP(label, content);
|
|
212
|
+
if (!verbose) {
|
|
213
|
+
// Compact response
|
|
214
|
+
return JSON.stringify({
|
|
215
|
+
score: result.score,
|
|
216
|
+
grade: result.grade,
|
|
217
|
+
summary: result.summary,
|
|
218
|
+
criticalCount: result.findings.filter((f) => f.severity === 'critical').length,
|
|
219
|
+
highCount: result.findings.filter((f) => f.severity === 'high').length,
|
|
220
|
+
recommendation: result.score >= 75
|
|
221
|
+
? 'Safe — minor issues can be addressed in follow-up'
|
|
222
|
+
: result.score >= 60
|
|
223
|
+
? 'Review recommended before merge'
|
|
224
|
+
: 'Block — resolve critical/high findings first',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return JSON.stringify(result, null, 2);
|
|
228
|
+
}
|
|
229
|
+
function handleScan(params) {
|
|
230
|
+
const text = String(params.text ?? '');
|
|
231
|
+
const findings = scanPayload(text);
|
|
232
|
+
const riskScore = calculateRiskScore(findings);
|
|
233
|
+
const criticalFindings = findings.filter((f) => f.severity === 'critical');
|
|
234
|
+
const blocked = criticalFindings.length > 0 || riskScore >= 60;
|
|
235
|
+
return JSON.stringify({
|
|
236
|
+
blocked,
|
|
237
|
+
riskScore,
|
|
238
|
+
decision: blocked ? 'block' : 'allow',
|
|
239
|
+
reason: blocked
|
|
240
|
+
? criticalFindings.length > 0
|
|
241
|
+
? `${criticalFindings[0].category}: ${criticalFindings[0].description}`
|
|
242
|
+
: `Risk score ${riskScore}/100 exceeds threshold`
|
|
243
|
+
: 'No threats detected',
|
|
244
|
+
findings: findings.slice(0, 10), // cap at 10 to keep response small
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Coerce a client-supplied numeric param to a bounded integer. Daemon
|
|
249
|
+
* endpoints take topN/limit straight from request bodies; a negative or NaN
|
|
250
|
+
* value silently corrupts slice() math (limit=-1 → slice keeps all-but-last,
|
|
251
|
+
* NaN → slice returns everything). Clamp to a sane range instead of trusting
|
|
252
|
+
* the wire.
|
|
253
|
+
*/
|
|
254
|
+
export function clampInt(v, def, min, max) {
|
|
255
|
+
// null/undefined = absent → default. Number(null) is 0 (finite), so without
|
|
256
|
+
// this guard a literal null would coerce to the min instead of the default.
|
|
257
|
+
if (v === null || v === undefined)
|
|
258
|
+
return def;
|
|
259
|
+
const n = Math.floor(Number(v));
|
|
260
|
+
if (!Number.isFinite(n))
|
|
261
|
+
return def;
|
|
262
|
+
return Math.max(min, Math.min(n, max));
|
|
263
|
+
}
|
|
264
|
+
function handleReview(params) {
|
|
265
|
+
const targetPath = String(params.path ?? process.cwd());
|
|
266
|
+
const topN = clampInt(params.topN, 10, 1, 100);
|
|
267
|
+
try {
|
|
268
|
+
const files = collectFilesForMCP(targetPath);
|
|
269
|
+
if (files.length === 0) {
|
|
270
|
+
return JSON.stringify({ message: 'No code files found', path: targetPath });
|
|
271
|
+
}
|
|
272
|
+
const results = files.map((f) => scoreFileForMCP(relative(targetPath, f.filepath), f.code));
|
|
273
|
+
results.sort((a, b) => a.score - b.score); // worst first
|
|
274
|
+
const top = results.slice(0, topN);
|
|
275
|
+
// Use the shared honesty floor (a critical finding caps the grade at F) so the
|
|
276
|
+
// MCP grade can never disagree with the CLI's grade for the same directory.
|
|
277
|
+
const { overallScore, grade } = aggregateTrust(results);
|
|
278
|
+
return JSON.stringify({
|
|
279
|
+
overallScore,
|
|
280
|
+
grade,
|
|
281
|
+
totalFiles: results.length,
|
|
282
|
+
riskiestFiles: top.map((r) => ({
|
|
283
|
+
filepath: r.filepath,
|
|
284
|
+
score: r.score,
|
|
285
|
+
grade: r.grade,
|
|
286
|
+
summary: r.summary,
|
|
287
|
+
criticalCount: r.findings.filter((f) => f.severity === 'critical').length,
|
|
288
|
+
highCount: r.findings.filter((f) => f.severity === 'high').length,
|
|
289
|
+
})),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
return JSON.stringify({ error: String(e) });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function handleAudit(params) {
|
|
297
|
+
const limit = clampInt(params.limit, 20, 1, 500);
|
|
298
|
+
const blockedOnly = Boolean(params.blockedOnly);
|
|
299
|
+
const overrideOnly = Boolean(params.overrideOnly);
|
|
300
|
+
const verify = Boolean(params.verify);
|
|
301
|
+
if (verify) {
|
|
302
|
+
// Tamper-evidence verification — walks the file, recomputes every hash,
|
|
303
|
+
// checks the chain. Returns full report including ALL break points.
|
|
304
|
+
const result = verifyAuditChain(AUDIT_FILE);
|
|
305
|
+
return JSON.stringify(result, null, 2);
|
|
306
|
+
}
|
|
307
|
+
if (!existsSync(AUDIT_FILE)) {
|
|
308
|
+
return JSON.stringify({
|
|
309
|
+
message: 'No audit log yet — install hooks first with: codehere hook install',
|
|
310
|
+
entries: [],
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const lines = readFileSync(AUDIT_FILE, 'utf-8')
|
|
314
|
+
.split('\n')
|
|
315
|
+
.filter(Boolean)
|
|
316
|
+
.slice(-Math.min(limit * 3, 500)); // read more than needed to account for filter
|
|
317
|
+
const entries = lines
|
|
318
|
+
.map((l) => {
|
|
319
|
+
try {
|
|
320
|
+
return JSON.parse(l);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
.filter(Boolean)
|
|
327
|
+
.filter((e) => {
|
|
328
|
+
if (overrideOnly)
|
|
329
|
+
return Boolean(e.overrideReason);
|
|
330
|
+
if (blockedOnly)
|
|
331
|
+
return Boolean(e.blocked);
|
|
332
|
+
return true;
|
|
333
|
+
})
|
|
334
|
+
.slice(-limit);
|
|
335
|
+
return JSON.stringify({ entries, total: entries.length });
|
|
336
|
+
}
|
|
337
|
+
// ── JSON-RPC dispatcher ──────────────────────────────────────────────────────
|
|
338
|
+
export function dispatch(req) {
|
|
339
|
+
const id = req.id;
|
|
340
|
+
const params = (req.params ?? {});
|
|
341
|
+
try {
|
|
342
|
+
switch (req.method) {
|
|
343
|
+
case 'initialize': {
|
|
344
|
+
// Echo the client's requested protocol version so Streamable-HTTP
|
|
345
|
+
// clients (Codex on 2025-x) negotiate; fall back to the baseline.
|
|
346
|
+
const clientPv = typeof params.protocolVersion === 'string' ? params.protocolVersion : '2024-11-05';
|
|
347
|
+
return {
|
|
348
|
+
jsonrpc: '2.0',
|
|
349
|
+
id,
|
|
350
|
+
result: {
|
|
351
|
+
protocolVersion: clientPv,
|
|
352
|
+
capabilities: { tools: {} },
|
|
353
|
+
serverInfo: { name: 'codehere', version: getVersion() },
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
case 'tools/list':
|
|
358
|
+
return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
|
|
359
|
+
case 'tools/call': {
|
|
360
|
+
const toolName = String(params.name ?? '');
|
|
361
|
+
const toolParams = (params.arguments ?? {});
|
|
362
|
+
let content;
|
|
363
|
+
switch (toolName) {
|
|
364
|
+
case 'codehere_trust':
|
|
365
|
+
content = handleTrust(toolParams);
|
|
366
|
+
break;
|
|
367
|
+
case 'codehere_scan':
|
|
368
|
+
content = handleScan(toolParams);
|
|
369
|
+
break;
|
|
370
|
+
case 'codehere_review':
|
|
371
|
+
content = handleReview(toolParams);
|
|
372
|
+
break;
|
|
373
|
+
case 'codehere_audit':
|
|
374
|
+
content = handleAudit(toolParams);
|
|
375
|
+
break;
|
|
376
|
+
case 'codehere_spend':
|
|
377
|
+
content = handleSpend(toolParams);
|
|
378
|
+
break;
|
|
379
|
+
case 'codehere_team':
|
|
380
|
+
content = handleTeam(toolParams);
|
|
381
|
+
break;
|
|
382
|
+
default:
|
|
383
|
+
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown tool: ${toolName}` } };
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
jsonrpc: '2.0',
|
|
387
|
+
id,
|
|
388
|
+
result: { content: [{ type: 'text', text: content }] },
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
case 'ping':
|
|
392
|
+
return { jsonrpc: '2.0', id, result: {} };
|
|
393
|
+
default:
|
|
394
|
+
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${req.method}` } };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (e) {
|
|
398
|
+
return { jsonrpc: '2.0', id, error: { code: -32603, message: String(e) } };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
//# sourceMappingURL=mcp-tools.js.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured artifacts — the comparison table, response mock and recommendation pick an agent
|
|
3
|
+
* produces, rendered from a typed data array rather than hand-authored HTML, plus the JSON
|
|
4
|
+
* tinting they use.
|
|
5
|
+
*
|
|
6
|
+
* Split out of app-script.ts (strangler fig, split 15). Verbatim slice, interpolated back where
|
|
7
|
+
* it was cut so it keeps sharing the surrounding IIFE scope. Verified by hashing the composed
|
|
8
|
+
* APP_SCRIPT before and after: same bytes.
|
|
9
|
+
*
|
|
10
|
+
* The primitive is real: these render from `t.artifacts.structured[]`, so what you see is what
|
|
11
|
+
* the adapter actually emitted. That is the difference between a demo and a record.
|
|
12
|
+
*/
|
|
13
|
+
export const ARTIFACTS = `// Structured artifacts · render the comparison table / response mock /
|
|
14
|
+
// recommendation pick that an agent produces, from a typed data array — NOT
|
|
15
|
+
// hand-authored HTML. The demo v12 weaves these into Phase 1; here they render
|
|
16
|
+
// from t.artifacts.structured[] so the primitive is real (seeded today, real
|
|
17
|
+
// adapter output later · demo-synth pattern).
|
|
18
|
+
function renderArtifacts(arr){
|
|
19
|
+
if(!Array.isArray(arr)||!arr.length)return'';
|
|
20
|
+
function he(s){ return String(s==null?'':s).replace(/[&<>"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
|
21
|
+
function tintJson(raw){
|
|
22
|
+
return he(raw)
|
|
23
|
+
.replace(/"([^&]*?)"(\\s*:)/g, '<span class="jk">"$1"</span>$2')
|
|
24
|
+
.replace(/:\\s*"([^&]*?)"/g, ': <span class="js">"$1"</span>')
|
|
25
|
+
.replace(/:\\s*(-?\\d+(?:\\.\\d+)?)/g, ': <span class="jn">$1</span>');
|
|
26
|
+
}
|
|
27
|
+
let out='<div class="panel"><div class="panel-title">Artifacts produced</div>';
|
|
28
|
+
for(const a of arr){
|
|
29
|
+
if(a.type==='comparison'){
|
|
30
|
+
const cols=a.columns||[];
|
|
31
|
+
let g='<div class="art-cmp" style="grid-template-columns:130px repeat('+cols.length+',1fr)">';
|
|
32
|
+
g+='<div class="hd"></div>'+cols.map(c=>'<div class="hd">'+he(c)+'</div>').join('');
|
|
33
|
+
for(const r of (a.rows||[])){
|
|
34
|
+
g+='<div class="rl">'+he(r.label)+'</div>';
|
|
35
|
+
for(const cell of (r.cells||[])){
|
|
36
|
+
const mk = cell.mark==='pick'?'pick':cell.mark==='meh'?'meh':'';
|
|
37
|
+
g+='<div class="cell">'+(mk?'<span class="'+mk+'">'+he(cell.text)+'</span>':he(cell.text))+'</div>';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
g+='</div>';
|
|
41
|
+
out+='<div class="artifact-card" data-user-content>'+(a.title?'<div class="artifact-h">'+he(a.title)+'</div>':'')+g+'</div>';
|
|
42
|
+
} else if(a.type==='mock'){
|
|
43
|
+
out+='<div class="art-mock">'
|
|
44
|
+
+'<div class="art-mock-head"><span class="status">'+he(a.status||'')+'</span>'+(a.contentType?'<span class="ct">'+he(a.contentType)+'</span>':'')+'</div>'
|
|
45
|
+
+'<div class="art-mock-body">'+tintJson(a.body||'')+'</div>'
|
|
46
|
+
+'</div>';
|
|
47
|
+
} else if(a.type==='pick'){
|
|
48
|
+
out+='<div class="art-pick">'
|
|
49
|
+
+'<div class="k">'+he(a.title||'')+'</div>'
|
|
50
|
+
+(a.what?'<div class="what">'+he(a.what)+'</div>':'')
|
|
51
|
+
+(a.why?'<div class="why">'+he(a.why)+'</div>':'')
|
|
52
|
+
+'</div>';
|
|
53
|
+
} else if(a.type==='bench'){
|
|
54
|
+
// Benchmark chart · horizontal bars · demo v12 .bench (phase audit 2.10)
|
|
55
|
+
let rows=(a.rows||[]).map(function(r){
|
|
56
|
+
const mk=r.mark==='allow'?' allow':r.mark==='warn'?' warn':'';
|
|
57
|
+
const pct=Math.max(0,Math.min(100,Number(r.pct)||0));
|
|
58
|
+
return '<div class="art-bench"><span class="bl">'+he(r.label)+'</span>'
|
|
59
|
+
+'<span class="bb"><span class="bf'+mk+'" style="width:'+pct+'%"></span></span>'
|
|
60
|
+
+'<span class="bv">'+he(r.value)+'</span></div>';
|
|
61
|
+
}).join('');
|
|
62
|
+
out+='<div class="artifact-card" data-user-content>'+(a.title?'<div class="artifact-h">'+he(a.title)+'</div>':'')+rows+'</div>';
|
|
63
|
+
} else if(a.type==='checklist'){
|
|
64
|
+
// Verifier severity grid · ✓/⚠ per check · demo v12 (phase audit 4.4/3.3)
|
|
65
|
+
let items=(a.items||[]).map(function(it){
|
|
66
|
+
const ok=it.ok!==false;
|
|
67
|
+
return '<div class="art-check'+(ok?'':' warn')+'">'
|
|
68
|
+
+'<span class="ci">'+(ok?'\\u2713':'\\u26A0')+'</span>'
|
|
69
|
+
+'<span class="cl">'+he(it.label)+'</span>'
|
|
70
|
+
+(it.note?'<span class="cn">'+he(it.note)+'</span>':'')
|
|
71
|
+
+'</div>';
|
|
72
|
+
}).join('');
|
|
73
|
+
out+='<div class="artifact-card" data-user-content>'+(a.title?'<div class="artifact-h">'+he(a.title)+'</div>':'')+'<div class="art-checklist">'+items+'</div></div>';
|
|
74
|
+
} else if(a.type==='sequence'){
|
|
75
|
+
// Sequence diagram · inline SVG · auto-layout actors + messages from a typed
|
|
76
|
+
// directive — NOT hand-authored SVG (demo v12 Artifact 2 · phase audit 1.13 · C-13).
|
|
77
|
+
const parts=(a.participants||[]).map(String);
|
|
78
|
+
const msgs=a.messages||[];
|
|
79
|
+
const n=parts.length;
|
|
80
|
+
if(n>=2){
|
|
81
|
+
const colW=148, padX=14, padTop=12, boxH=26, headGap=20, rowH=34, padBot=16, loopW=42;
|
|
82
|
+
const cx=i=>padX+74+colW*i; // center x of lifeline i
|
|
83
|
+
const w=padX*2+148+colW*(n-1)+loopW; // right margin covers a trailing self-loop
|
|
84
|
+
const topY=padTop+boxH;
|
|
85
|
+
const h=topY+headGap+rowH*msgs.length+padBot;
|
|
86
|
+
const cls=m=>m.mark==='allow'?'allow':m.mark==='warn'?'warn':m.ret?'ret':'';
|
|
87
|
+
const head=m=>m.mark==='allow'?'ah-allow':m.mark==='warn'?'ah-warn':m.ret?'ah-ret':'ah';
|
|
88
|
+
let svg='<svg class="seq-svg" viewBox="0 0 '+w+' '+h+'" width="'+w+'" height="'+h+'" preserveAspectRatio="xMidYMin meet">';
|
|
89
|
+
svg+='<defs>';
|
|
90
|
+
['ah','ah-allow','ah-warn','ah-ret'].forEach(function(id){
|
|
91
|
+
svg+='<marker id="seq-'+id+'" markerWidth="9" markerHeight="9" refX="6.5" refY="3.5" orient="auto"><path class="'+id+'" d="M0,0 L7,3.5 L0,7 Z"/></marker>';
|
|
92
|
+
});
|
|
93
|
+
svg+='</defs>';
|
|
94
|
+
for(let i=0;i<n;i++){
|
|
95
|
+
const x=cx(i), bw=Math.max(76,parts[i].length*7+18);
|
|
96
|
+
svg+='<line class="ll" x1="'+x+'" y1="'+topY+'" x2="'+x+'" y2="'+(h-padBot)+'"/>';
|
|
97
|
+
svg+='<rect class="box" x="'+(x-bw/2)+'" y="'+padTop+'" width="'+bw+'" height="'+boxH+'" rx="5"/>';
|
|
98
|
+
svg+='<text class="at" x="'+x+'" y="'+(padTop+boxH/2+4)+'" text-anchor="middle" font-size="11" font-family="ui-monospace,monospace">'+he(parts[i])+'</text>';
|
|
99
|
+
}
|
|
100
|
+
msgs.forEach(function(m,mi){
|
|
101
|
+
const fi=typeof m.from==='number'?m.from:parts.indexOf(m.from);
|
|
102
|
+
const ti=typeof m.to==='number'?m.to:parts.indexOf(m.to);
|
|
103
|
+
if(fi<0||ti<0)return;
|
|
104
|
+
const y=topY+headGap+rowH*mi+rowH/2, x1=cx(fi), x2=cx(ti), c=cls(m), tc=(c==='allow'||c==='warn')?' '+c:'';
|
|
105
|
+
if(fi===ti){ // self-message · small loop to the right
|
|
106
|
+
svg+='<path class="msg '+c+'" d="M'+x1+','+(y-5)+' h'+loopW+' v10 h-'+loopW+'" marker-end="url(#seq-'+head(m)+')"/>';
|
|
107
|
+
svg+='<text class="mt'+tc+'" x="'+(x1+loopW+5)+'" y="'+(y-8)+'" font-size="10" font-family="ui-monospace,monospace">'+he(m.label||'')+'</text>';
|
|
108
|
+
} else {
|
|
109
|
+
svg+='<line class="msg '+c+'" x1="'+x1+'" y1="'+y+'" x2="'+x2+'" y2="'+y+'" marker-end="url(#seq-'+head(m)+')"/>';
|
|
110
|
+
svg+='<text class="mt'+tc+'" x="'+((x1+x2)/2)+'" y="'+(y-6)+'" text-anchor="middle" font-size="10" font-family="ui-monospace,monospace">'+he(m.label||'')+'</text>';
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
svg+='</svg>';
|
|
114
|
+
out+='<div class="artifact-card" data-user-content>'+(a.title?'<div class="artifact-h">'+he(a.title)+'</div>':'')+'<div class="art-seq">'+svg+'</div></div>';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
out+='</div>';
|
|
119
|
+
return out;
|
|
120
|
+
}`;
|
|
121
|
+
//# sourceMappingURL=app-script-artifacts.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit-chain footer stats and activity dots — the always-visible summary of how much the
|
|
3
|
+
* chain has recorded and whether it is still intact.
|
|
4
|
+
*
|
|
5
|
+
* Split out of app-script.ts (strangler fig). Verbatim slice, interpolated back at the
|
|
6
|
+
* position it was cut from so it keeps sharing the surrounding IIFE scope — its functions
|
|
7
|
+
* are still called from the code left behind. Verified by hashing the composed APP_SCRIPT
|
|
8
|
+
* before and after: same bytes.
|
|
9
|
+
*/
|
|
10
|
+
export const AUDIT_FOOTER = ` /* Audit-chain footer stats + activity dots · enriches the footer
|
|
11
|
+
from a one-liner to a real status panel. Polls /api/audit/summary
|
|
12
|
+
every 8s. */
|
|
13
|
+
async function pollAuditStats(){
|
|
14
|
+
try{
|
|
15
|
+
const r = await fetch('/api/audit/summary');
|
|
16
|
+
if(!r.ok) return;
|
|
17
|
+
const d = await r.json();
|
|
18
|
+
const statsEl = document.getElementById('audit-chain-stats');
|
|
19
|
+
const dotsEl = document.getElementById('audit-chain-dots');
|
|
20
|
+
if(statsEl){
|
|
21
|
+
const actions = d.actions || 0;
|
|
22
|
+
const blocked = d.blocked || 0;
|
|
23
|
+
const agents = d.agents || 0;
|
|
24
|
+
statsEl.textContent = actions+' entries last 24h · '+agents+' agent'+(agents===1?'':'s')+' · '+blocked+' blocked';
|
|
25
|
+
}
|
|
26
|
+
if(dotsEl){
|
|
27
|
+
// Without per-entry recent[], approximate by drawing N=min(actions,12)
|
|
28
|
+
// green dots and B=min(blocked, 12) red dots. Honest enough · still
|
|
29
|
+
// gives the user visual "audit chain is alive" signal.
|
|
30
|
+
const a = Math.min(d.actions||0, 12);
|
|
31
|
+
const b = Math.min(d.blocked||0, 12);
|
|
32
|
+
const greens = Math.max(0, a - b);
|
|
33
|
+
let h = '';
|
|
34
|
+
for(let i=0;i<b;i++) h += '<span class="dot block" title="blocked entry"></span>';
|
|
35
|
+
for(let i=0;i<greens;i++) h += '<span class="dot allow" title="allowed entry"></span>';
|
|
36
|
+
if(!a) for(let i=0;i<3;i++) h += '<span class="dot muted" title="no entries yet"></span>';
|
|
37
|
+
dotsEl.innerHTML = h;
|
|
38
|
+
}
|
|
39
|
+
}catch{ /* ignore */ }
|
|
40
|
+
}
|
|
41
|
+
setInterval(pollAuditStats, 8000);
|
|
42
|
+
pollAuditStats();
|
|
43
|
+
`;
|
|
44
|
+
//# sourceMappingURL=app-script-audit-footer.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit-chain verification — the dashboard button that asks the daemon to re-walk the hash
|
|
3
|
+
* chain and renders the verdict it gets back.
|
|
4
|
+
*
|
|
5
|
+
* Split out of app-script.ts (strangler fig). Verbatim slice, interpolated back at the
|
|
6
|
+
* position it was cut from so it keeps sharing the surrounding IIFE scope. Verified by
|
|
7
|
+
* hashing the composed APP_SCRIPT before and after: same bytes.
|
|
8
|
+
*/
|
|
9
|
+
export const AUDIT_CHAIN_VERIFY = `// F-06 · audit chain verification · click handler for the dashboard panel.
|
|
10
|
+
async function verifyAuditChain(){
|
|
11
|
+
const btn=document.getElementById('audit-verify-btn');
|
|
12
|
+
const res=document.getElementById('audit-result');
|
|
13
|
+
if(!btn||!res)return;
|
|
14
|
+
btn.textContent='Verifying...';
|
|
15
|
+
btn.className='audit-chain-btn';
|
|
16
|
+
try{
|
|
17
|
+
const r=await fetch('/api/audit/verify').then(x=>x.json());
|
|
18
|
+
if(r.error){
|
|
19
|
+
res.innerHTML='<span class="fail">Failed: '+String(r.error).replace(/</g,'<')+'</span>';
|
|
20
|
+
btn.textContent='Verify chain'; return;
|
|
21
|
+
}
|
|
22
|
+
const counts='<div class="audit-chain-counts">'+r.totalEntries+' entries · '+r.chainedEntries+' hash-chained · '+r.legacyEntries+' legacy</div>';
|
|
23
|
+
// Ask "is there anything here?" BEFORE "did it pass?", and never make the empty branch
|
|
24
|
+
// depend on r.valid.
|
|
25
|
+
//
|
|
26
|
+
// This read \`r.valid && totalEntries===0\`, which was correct against the OLD API — back
|
|
27
|
+
// when an empty chain returned valid:true, the lie that f030769 fixed. Making the API
|
|
28
|
+
// honest (valid:false when there is nothing to verify) turned this branch unreachable, so
|
|
29
|
+
// a fresh install clicking "verify log" got "× Chain BROKEN · 0 break point(s) detected"
|
|
30
|
+
// in red. Self-contradictory on its face, and alarming for someone whose only mistake was
|
|
31
|
+
// not having run an agent yet.
|
|
32
|
+
//
|
|
33
|
+
// Fixing valid without walking every consumer of valid is how a two-state control over a
|
|
34
|
+
// three-state fact keeps producing a new lie in a new place. Empty is its own answer.
|
|
35
|
+
if((r.totalEntries||0)===0 || r.verifiable===false){
|
|
36
|
+
res.innerHTML='<div class="audit-chain-counts">Nothing recorded yet · run an agent through codehere to start the chain.</div>';
|
|
37
|
+
btn.textContent='Verify chain';
|
|
38
|
+
btn.className='audit-chain-btn';
|
|
39
|
+
} else if(r.valid){
|
|
40
|
+
res.innerHTML='<span class="ok">✓ Chain intact · every entry verified against its predecessor hash.</span>'+counts;
|
|
41
|
+
btn.textContent='Chain intact';
|
|
42
|
+
btn.className='audit-chain-btn verified';
|
|
43
|
+
} else {
|
|
44
|
+
const errs=(r.errors||[]).slice(0,5).map(function(e){
|
|
45
|
+
return '<div class="audit-chain-error">line '+e.lineNumber+' · '+e.reason+' · '+String(e.detail||'').replace(/</g,'<')+'</div>';
|
|
46
|
+
}).join('');
|
|
47
|
+
res.innerHTML='<span class="fail">× Chain BROKEN · '+(r.errors||[]).length+' break point(s) detected.</span>'+counts+errs;
|
|
48
|
+
btn.textContent='Re-verify';
|
|
49
|
+
btn.className='audit-chain-btn broken';
|
|
50
|
+
}
|
|
51
|
+
}catch(e){
|
|
52
|
+
res.innerHTML='<span class="fail">Failed to reach /api/audit/verify: '+(e&&e.message?e.message:String(e))+'</span>';
|
|
53
|
+
btn.textContent='Verify chain';
|
|
54
|
+
}
|
|
55
|
+
}`;
|
|
56
|
+
//# sourceMappingURL=app-script-audit-verify.js.map
|