sneakoscope 4.1.1 → 4.2.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 +13 -10
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/crates/sks-core/src/main.rs +1 -1
- package/dist/bin/sks.js +1 -1
- package/dist/cli/command-registry.js +1 -1
- package/dist/core/auto-review.js +1 -1
- package/dist/core/codex-control/codex-app-server-v2-client.js +86 -2
- package/dist/core/codex-control/codex-reliability-shield.js +26 -5
- package/dist/core/codex-control/codex-task-runner.js +7 -1
- package/dist/core/codex-control/model-call-concurrency.js +1 -1
- package/dist/core/commands/mad-db-command.js +146 -51
- package/dist/core/commands/mad-sks-command.js +15 -31
- package/dist/core/commands/qa-loop-command.js +23 -7
- package/dist/core/db-safety.js +35 -37
- package/dist/core/doctor/supabase-mcp-repair.js +2 -2
- package/dist/core/feature-registry.js +1 -1
- package/dist/core/fsx.js +1 -1
- package/dist/core/hooks-runtime.js +1 -1
- package/dist/core/init.js +5 -4
- package/dist/core/mad-db/mad-db-capability.js +203 -74
- package/dist/core/mad-db/mad-db-coordinator.js +287 -0
- package/dist/core/mad-db/mad-db-executor.js +156 -0
- package/dist/core/mad-db/mad-db-ledger.js +1 -1
- package/dist/core/mad-db/mad-db-lock.js +40 -0
- package/dist/core/mad-db/mad-db-operation-store.js +140 -0
- package/dist/core/mad-db/mad-db-policy-resolver.js +42 -22
- package/dist/core/mad-db/mad-db-policy.js +195 -0
- package/dist/core/mad-db/mad-db-postconditions.js +30 -0
- package/dist/core/mad-db/mad-db-recovery.js +27 -0
- package/dist/core/mad-db/mad-db-result-lifecycle.js +31 -102
- package/dist/core/mad-db/mad-db-runtime-profile.js +121 -0
- package/dist/core/mad-db/mad-db-target.js +64 -0
- package/dist/core/managed-assets/managed-assets-manifest.js +1 -1
- package/dist/core/pipeline-internals/runtime-core.js +40 -0
- package/dist/core/providers/glm/bench/glm-benchmark-types.js +1 -1
- package/dist/core/qa-loop/qa-app-server-driver.js +134 -0
- package/dist/core/qa-loop/qa-contract-v2.js +231 -0
- package/dist/core/qa-loop/qa-gate-v2.js +132 -0
- package/dist/core/qa-loop/qa-runtime-artifacts.js +53 -0
- package/dist/core/qa-loop/qa-surface-router.js +114 -0
- package/dist/core/qa-loop/qa-types.js +18 -0
- package/dist/core/qa-loop.js +83 -26
- package/dist/core/release/gate-manifest.js +1 -0
- package/dist/core/release/release-gate-dag.js +6 -5
- package/dist/core/release/sla-scheduler.js +1 -1
- package/dist/core/routes.js +42 -12
- package/dist/core/triwiki/triwiki-affected-graph.js +3 -2
- package/dist/core/version.js +1 -1
- package/dist/core/zellij/zellij-slot-column-anchor.js +5 -1
- package/dist/scripts/check-dist-runtime.js +3 -2
- package/dist/scripts/codex-0142-manifest-check.js +2 -1
- package/dist/scripts/codex-control-all-pipelines-check.js +1 -0
- package/dist/scripts/codex-control-model-capacity-fallback-check.js +53 -0
- package/dist/scripts/config-managed-merge-callsite-coverage-check.js +7 -1
- package/dist/scripts/loop-directive-check-lib.js +78 -1
- package/dist/scripts/mad-db-capability-check.js +13 -2
- package/dist/scripts/mad-db-command-check.js +7 -5
- package/dist/scripts/mad-db-hook-idempotency-check.js +21 -0
- package/dist/scripts/mad-db-ledger-check.js +2 -1
- package/dist/scripts/mad-db-lifecycle-hook-decision-check.js +5 -4
- package/dist/scripts/mad-db-mad-command-check.js +29 -16
- package/dist/scripts/mad-db-mcp-result-lifecycle-check.js +11 -10
- package/dist/scripts/mad-db-one-cycle-bounded-check.js +15 -18
- package/dist/scripts/mad-db-one-cycle-consumption-check.js +3 -3
- package/dist/scripts/mad-db-operation-lifecycle-blackbox.js +9 -9
- package/dist/scripts/mad-db-operation-lifecycle-ledger-check.js +6 -6
- package/dist/scripts/mad-db-parallel-lifecycle-check.js +24 -0
- package/dist/scripts/mad-db-policy-v2-check.js +20 -0
- package/dist/scripts/mad-db-priority-resolver-check.js +5 -5
- package/dist/scripts/mad-db-real-supabase-e2e.js +166 -0
- package/dist/scripts/mad-db-route-identity-check.js +28 -0
- package/dist/scripts/mad-db-runtime-profile-lifecycle-check.js +24 -0
- package/dist/scripts/mad-db-safety-conflict-matrix-check.js +3 -3
- package/dist/scripts/mad-db-skill-policy-snapshot-check.js +15 -0
- package/dist/scripts/qa-loop-app-server-driver-check.js +74 -0
- package/dist/scripts/qa-loop-surface-router-check.js +49 -0
- package/dist/scripts/release-check-dynamic-execute.js +1 -1
- package/dist/scripts/release-dag-full-coverage-check.js +6 -0
- package/dist/scripts/release-triwiki-first-runner-blackbox.js +5 -1
- package/dist/scripts/runtime-ts-rust-boundary-check.js +1 -1
- package/dist/scripts/triwiki-affected-graph-check.js +2 -2
- package/package.json +18 -5
- package/schemas/mad-db/mad-db-capability.schema.json +92 -19
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { nowIso, sha256 } from '../fsx.js';
|
|
3
|
+
export class MadDbMcpExecutor {
|
|
4
|
+
profile;
|
|
5
|
+
opts;
|
|
6
|
+
client = null;
|
|
7
|
+
transport = null;
|
|
8
|
+
constructor(profile, opts = {}) {
|
|
9
|
+
this.profile = profile;
|
|
10
|
+
this.opts = opts;
|
|
11
|
+
}
|
|
12
|
+
async connect() {
|
|
13
|
+
if (this.client)
|
|
14
|
+
return;
|
|
15
|
+
const { Client, StreamableHTTPClientTransport } = await loadMcpSdk();
|
|
16
|
+
this.client = new Client({ name: 'sneakoscope-mad-db', version: '4.2.0' });
|
|
17
|
+
const headers = authHeaders();
|
|
18
|
+
const options = headers ? { requestInit: { headers } } : {};
|
|
19
|
+
this.transport = new StreamableHTTPClientTransport(new URL(this.profile.server_url), options);
|
|
20
|
+
await this.client.connect(this.transport);
|
|
21
|
+
}
|
|
22
|
+
async inventory() {
|
|
23
|
+
const started = performance.now();
|
|
24
|
+
try {
|
|
25
|
+
await this.connect();
|
|
26
|
+
const result = await this.client.listTools({}, { timeout: this.opts.timeoutMs || 10_000 });
|
|
27
|
+
const names = (result.tools || []).map((tool) => String(tool?.name || '')).filter(Boolean).sort();
|
|
28
|
+
return {
|
|
29
|
+
schema: 'sks.mad-db-tool-inventory.v1',
|
|
30
|
+
checked_at: nowIso(),
|
|
31
|
+
ok: hasTool(names, 'execute_sql') && hasTool(names, 'apply_migration'),
|
|
32
|
+
tool_names: names,
|
|
33
|
+
execute_sql_available: hasTool(names, 'execute_sql'),
|
|
34
|
+
apply_migration_available: hasTool(names, 'apply_migration'),
|
|
35
|
+
duration_ms: Math.round(performance.now() - started),
|
|
36
|
+
error_digest: null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return {
|
|
41
|
+
schema: 'sks.mad-db-tool-inventory.v1',
|
|
42
|
+
checked_at: nowIso(),
|
|
43
|
+
ok: false,
|
|
44
|
+
tool_names: [],
|
|
45
|
+
execute_sql_available: false,
|
|
46
|
+
apply_migration_available: false,
|
|
47
|
+
duration_ms: Math.round(performance.now() - started),
|
|
48
|
+
error_digest: sha256(redactError(err)).slice(0, 32)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async executeSql(sql) {
|
|
53
|
+
return this.callToolWithFallback(['execute_sql', 'supabase.execute_sql'], [{ query: sql }, { sql }]);
|
|
54
|
+
}
|
|
55
|
+
async applyMigration(name, sql) {
|
|
56
|
+
return this.callToolWithFallback(['apply_migration', 'supabase.apply_migration'], [{ name, query: sql }, { name, sql }]);
|
|
57
|
+
}
|
|
58
|
+
async close() {
|
|
59
|
+
await this.transport?.terminateSession().catch(() => undefined);
|
|
60
|
+
await this.client?.close().catch(() => undefined);
|
|
61
|
+
this.client = null;
|
|
62
|
+
this.transport = null;
|
|
63
|
+
}
|
|
64
|
+
async callToolWithFallback(toolNames, argsList) {
|
|
65
|
+
await this.connect();
|
|
66
|
+
const inventory = await this.inventory();
|
|
67
|
+
const tool = toolNames.find((name) => hasTool(inventory.tool_names, name)) || toolNames[0] || '';
|
|
68
|
+
let lastError = null;
|
|
69
|
+
for (const args of argsList) {
|
|
70
|
+
const started = performance.now();
|
|
71
|
+
try {
|
|
72
|
+
const result = await this.client.callTool({ name: tool, arguments: args }, undefined, { timeout: this.opts.timeoutMs || 60_000, resetTimeoutOnProgress: true, maxTotalTimeout: 10 * 60_000 });
|
|
73
|
+
return {
|
|
74
|
+
schema: 'sks.mad-db-tool-result.v1',
|
|
75
|
+
ok: result.isError !== true,
|
|
76
|
+
tool_name: tool,
|
|
77
|
+
result_digest: sha256(JSON.stringify(redactToolResult(result))).slice(0, 32),
|
|
78
|
+
row_count: extractRowCount(result),
|
|
79
|
+
is_error: result.isError === true,
|
|
80
|
+
duration_ms: Math.round(performance.now() - started)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
lastError = err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
schema: 'sks.mad-db-tool-result.v1',
|
|
89
|
+
ok: false,
|
|
90
|
+
tool_name: tool,
|
|
91
|
+
result_digest: sha256(redactError(lastError)).slice(0, 32),
|
|
92
|
+
row_count: null,
|
|
93
|
+
is_error: true,
|
|
94
|
+
duration_ms: 0
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function loadMcpSdk() {
|
|
99
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
100
|
+
const [{ Client }, { StreamableHTTPClientTransport }] = await Promise.all([
|
|
101
|
+
dynamicImport('@modelcontextprotocol/sdk/client/index.js'),
|
|
102
|
+
dynamicImport('@modelcontextprotocol/sdk/client/streamableHttp.js')
|
|
103
|
+
]);
|
|
104
|
+
return { Client, StreamableHTTPClientTransport };
|
|
105
|
+
}
|
|
106
|
+
function hasTool(names, name) {
|
|
107
|
+
return names.some((candidate) => candidate === name || candidate.endsWith(`.${name}`) || candidate.endsWith(`__${name}`));
|
|
108
|
+
}
|
|
109
|
+
function authHeaders() {
|
|
110
|
+
const token = process.env.SUPABASE_ACCESS_TOKEN || process.env.SKS_MAD_DB_SUPABASE_ACCESS_TOKEN || '';
|
|
111
|
+
if (!token)
|
|
112
|
+
return undefined;
|
|
113
|
+
return { Authorization: `Bearer ${token}` };
|
|
114
|
+
}
|
|
115
|
+
function redactError(err) {
|
|
116
|
+
const text = err instanceof Error ? `${err.name}:${err.message}` : String(err);
|
|
117
|
+
return text.replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, '$1<redacted>').replace(/(token|password|secret|apikey)=([^&\s]+)/gi, '$1=<redacted>');
|
|
118
|
+
}
|
|
119
|
+
function redactToolResult(value) {
|
|
120
|
+
if (value == null)
|
|
121
|
+
return value;
|
|
122
|
+
if (typeof value === 'string')
|
|
123
|
+
return value.replace(/(token|password|secret|apikey)["'=:\s]+[A-Za-z0-9._~+/=-]+/gi, '$1=<redacted>');
|
|
124
|
+
if (Array.isArray(value))
|
|
125
|
+
return value.map(redactToolResult);
|
|
126
|
+
if (typeof value === 'object') {
|
|
127
|
+
const out = {};
|
|
128
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
129
|
+
if (/token|password|secret|apikey|service_role/i.test(key))
|
|
130
|
+
out[key] = '<redacted>';
|
|
131
|
+
else if (/rows|data|records/i.test(key) && Array.isArray(entry))
|
|
132
|
+
out[key] = `<redacted:${entry.length}:rows>`;
|
|
133
|
+
else
|
|
134
|
+
out[key] = redactToolResult(entry);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
function extractRowCount(value) {
|
|
141
|
+
const candidates = [
|
|
142
|
+
value?.row_count,
|
|
143
|
+
value?.rowCount,
|
|
144
|
+
value?.rows_affected,
|
|
145
|
+
value?.structuredContent?.row_count,
|
|
146
|
+
value?.structuredContent?.rowCount,
|
|
147
|
+
value?.structuredContent?.rows_affected
|
|
148
|
+
];
|
|
149
|
+
for (const candidate of candidates) {
|
|
150
|
+
const n = Number(candidate);
|
|
151
|
+
if (Number.isFinite(n))
|
|
152
|
+
return n;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=mad-db-executor.js.map
|
|
@@ -23,7 +23,7 @@ export async function appendMadDbOperationLifecycle(root, missionId, input) {
|
|
|
23
23
|
tool_name: input.toolName || null,
|
|
24
24
|
sql_hash: input.sqlHash || null,
|
|
25
25
|
destructive: input.destructive === true,
|
|
26
|
-
result_status: input.resultStatus || '
|
|
26
|
+
result_status: input.resultStatus || 'pending_tool_result',
|
|
27
27
|
row_count: input.rowCount ?? null,
|
|
28
28
|
error: input.error || null
|
|
29
29
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ensureDir, nowIso, writeJsonAtomic } from '../fsx.js';
|
|
4
|
+
import { missionDir } from '../mission.js';
|
|
5
|
+
export async function withMadDbLock(root, missionId, name, fn) {
|
|
6
|
+
const dir = path.join(missionDir(root, missionId), 'mad-db', 'runtime', 'locks');
|
|
7
|
+
await ensureDir(dir);
|
|
8
|
+
const lockDir = path.join(dir, `${safeName(name)}.lock`);
|
|
9
|
+
const deadline = Date.now() + 10_000;
|
|
10
|
+
while (true) {
|
|
11
|
+
try {
|
|
12
|
+
await fs.mkdir(lockDir);
|
|
13
|
+
await writeJsonAtomic(path.join(lockDir, 'owner.json'), {
|
|
14
|
+
schema: 'sks.mad-db-lock.v1',
|
|
15
|
+
pid: process.pid,
|
|
16
|
+
name,
|
|
17
|
+
acquired_at: nowIso()
|
|
18
|
+
});
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
if (Date.now() > deadline)
|
|
23
|
+
throw new Error(`mad_db_lock_timeout:${name}`);
|
|
24
|
+
await sleep(25 + Math.floor(Math.random() * 25));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return await fn();
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
await fs.rm(lockDir, { recursive: true, force: true }).catch(() => undefined);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function sleep(ms) {
|
|
35
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
}
|
|
37
|
+
function safeName(value) {
|
|
38
|
+
return value.replace(/[^a-z0-9_.-]+/gi, '_').slice(0, 80) || 'lock';
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=mad-db-lock.js.map
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { ensureDir, nowIso, readJson, sha256, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { missionDir } from '../mission.js';
|
|
4
|
+
import { appendMadDbLedgerEvent } from './mad-db-ledger.js';
|
|
5
|
+
import { withMadDbLock } from './mad-db-lock.js';
|
|
6
|
+
import { updateMadDbCapabilityCounters } from './mad-db-capability.js';
|
|
7
|
+
export async function reserveMadDbOperation(input) {
|
|
8
|
+
return withMadDbLock(input.root, input.missionId, `operation-${safeKey(input.toolCallId)}`, async () => {
|
|
9
|
+
const file = operationFile(input.root, input.missionId, input.toolCallId);
|
|
10
|
+
const existing = await readJson(file, null);
|
|
11
|
+
if (existing?.schema === 'sks.mad-db-operation.v2') {
|
|
12
|
+
return { operation: existing, reused: true, capability: input.capability };
|
|
13
|
+
}
|
|
14
|
+
const operationId = `mad-db-op-${sha256(`${input.missionId}:${input.capability.cycle_id}:${input.toolCallId}`).slice(0, 16)}`;
|
|
15
|
+
const operation = {
|
|
16
|
+
schema: 'sks.mad-db-operation.v2',
|
|
17
|
+
operation_id: operationId,
|
|
18
|
+
tool_call_id: input.toolCallId,
|
|
19
|
+
mission_id: input.missionId,
|
|
20
|
+
cycle_id: input.capability.cycle_id,
|
|
21
|
+
project_ref_hash: sha256(input.capability.project_ref).slice(0, 16),
|
|
22
|
+
tool_name: input.toolName,
|
|
23
|
+
sql_sha256: input.sql ? sha256(input.sql) : null,
|
|
24
|
+
migration_name: input.migrationName || null,
|
|
25
|
+
operation_classes: input.operationClasses,
|
|
26
|
+
state: 'reserved',
|
|
27
|
+
attempt: 1,
|
|
28
|
+
started_at: null,
|
|
29
|
+
finished_at: null,
|
|
30
|
+
result_digest: null,
|
|
31
|
+
verification_artifact: null,
|
|
32
|
+
error_code: null
|
|
33
|
+
};
|
|
34
|
+
await ensureDir(path.dirname(file));
|
|
35
|
+
await writeJsonAtomic(file, operation);
|
|
36
|
+
const capability = await updateMadDbCapabilityCounters(input.root, input.missionId, {
|
|
37
|
+
attemptsDelta: 1,
|
|
38
|
+
reservedDelta: 1
|
|
39
|
+
});
|
|
40
|
+
await appendMadDbLedgerEvent(input.root, input.missionId, {
|
|
41
|
+
type: 'db_operation.reserved',
|
|
42
|
+
operation_id: operation.operation_id,
|
|
43
|
+
tool_call_id: operation.tool_call_id,
|
|
44
|
+
cycle_id: operation.cycle_id,
|
|
45
|
+
tool_name: operation.tool_name,
|
|
46
|
+
sql_sha256: operation.sql_sha256,
|
|
47
|
+
operation_classes: operation.operation_classes
|
|
48
|
+
});
|
|
49
|
+
return { operation, reused: false, capability: capability || input.capability };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export async function transitionMadDbOperation(input) {
|
|
53
|
+
return withMadDbLock(input.root, input.missionId, `operation-${safeKey(input.toolCallId)}`, async () => {
|
|
54
|
+
const file = operationFile(input.root, input.missionId, input.toolCallId);
|
|
55
|
+
const existing = await readJson(file, null);
|
|
56
|
+
if (!existing)
|
|
57
|
+
return null;
|
|
58
|
+
const terminalSuccess = input.state === 'succeeded' || input.state === 'verified';
|
|
59
|
+
const terminalFailure = input.state === 'failed' || input.state === 'verification_failed' || input.state === 'unknown';
|
|
60
|
+
const updated = {
|
|
61
|
+
...existing,
|
|
62
|
+
state: input.state,
|
|
63
|
+
started_at: existing.started_at || (input.state === 'started' ? nowIso() : existing.started_at),
|
|
64
|
+
finished_at: terminalSuccess || terminalFailure ? nowIso() : existing.finished_at,
|
|
65
|
+
result_digest: input.result === undefined ? existing.result_digest : sha256(safeResultDigestInput(input.result)).slice(0, 32),
|
|
66
|
+
verification_artifact: input.verificationArtifact || existing.verification_artifact,
|
|
67
|
+
error_code: input.errorCode || existing.error_code
|
|
68
|
+
};
|
|
69
|
+
await writeJsonAtomic(file, updated);
|
|
70
|
+
if (input.state === 'succeeded')
|
|
71
|
+
await updateMadDbCapabilityCounters(input.root, input.missionId, { succeededDelta: 1 });
|
|
72
|
+
if (input.state === 'failed')
|
|
73
|
+
await updateMadDbCapabilityCounters(input.root, input.missionId, { failedDelta: 1 });
|
|
74
|
+
await appendMadDbLedgerEvent(input.root, input.missionId, {
|
|
75
|
+
type: `db_operation.${input.state}`,
|
|
76
|
+
operation_id: updated.operation_id,
|
|
77
|
+
tool_call_id: updated.tool_call_id,
|
|
78
|
+
cycle_id: updated.cycle_id,
|
|
79
|
+
tool_name: updated.tool_name,
|
|
80
|
+
result_digest: updated.result_digest,
|
|
81
|
+
verification_artifact: updated.verification_artifact,
|
|
82
|
+
error_code: updated.error_code
|
|
83
|
+
});
|
|
84
|
+
return updated;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
export function extractCanonicalToolCallId(payload = {}) {
|
|
88
|
+
const candidates = [
|
|
89
|
+
payload.tool_call_id,
|
|
90
|
+
payload.toolCallId,
|
|
91
|
+
payload.call_id,
|
|
92
|
+
payload.callId,
|
|
93
|
+
payload.id,
|
|
94
|
+
payload.request_id,
|
|
95
|
+
payload.requestId,
|
|
96
|
+
payload.tool?.id,
|
|
97
|
+
payload.tool?.call_id,
|
|
98
|
+
payload.metadata?.tool_call_id,
|
|
99
|
+
payload.context?.tool_call_id
|
|
100
|
+
];
|
|
101
|
+
const found = candidates.find((value) => typeof value === 'string' && value.trim());
|
|
102
|
+
return found ? String(found).trim() : null;
|
|
103
|
+
}
|
|
104
|
+
export function operationFile(root, missionId, toolCallId) {
|
|
105
|
+
return path.join(missionDir(root, missionId), 'mad-db', 'runtime', 'operations', `${safeKey(toolCallId)}.json`);
|
|
106
|
+
}
|
|
107
|
+
function safeKey(value) {
|
|
108
|
+
return sha256(value).slice(0, 24);
|
|
109
|
+
}
|
|
110
|
+
function safeResultDigestInput(value) {
|
|
111
|
+
try {
|
|
112
|
+
return JSON.stringify(redactSensitive(value));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return String(value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function redactSensitive(value, depth = 0) {
|
|
119
|
+
if (depth > 8 || value == null)
|
|
120
|
+
return value;
|
|
121
|
+
if (typeof value === 'string') {
|
|
122
|
+
return value.replace(/(access_token|refresh_token|password|apikey|service_role|secret)["'=:\s]+[A-Za-z0-9._~+/=-]+/gi, '$1=<redacted>');
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(value))
|
|
125
|
+
return value.map((entry) => redactSensitive(entry, depth + 1));
|
|
126
|
+
if (typeof value === 'object') {
|
|
127
|
+
const out = {};
|
|
128
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
129
|
+
if (/token|password|secret|apikey|service_role/i.test(key))
|
|
130
|
+
out[key] = '<redacted>';
|
|
131
|
+
else if (/rows|data|records/i.test(key) && Array.isArray(entry))
|
|
132
|
+
out[key] = `<redacted:${entry.length}:rows>`;
|
|
133
|
+
else
|
|
134
|
+
out[key] = redactSensitive(entry, depth + 1);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=mad-db-operation-store.js.map
|
|
@@ -1,30 +1,56 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { isMadDbCapabilityActive, readMadDbCapability } from './mad-db-capability.js';
|
|
2
|
+
import { activeMadDbAllowsSqlPlane, isMadDbControlPlaneDeniedTool, madDbOperationClassesFromClassification } from './mad-db-policy.js';
|
|
3
|
+
import { sha256 } from '../fsx.js';
|
|
4
|
+
export const MAD_DB_POLICY_DECISION_SCHEMA = 'sks.mad-db-policy-decision.v2';
|
|
5
|
+
export async function resolveMadDbMutationPolicy(root, state = {}, classification = {}, explicitCapability) {
|
|
6
|
+
const missionId = explicitCapability?.mission_id || state?.mad_db_capability_mission_id || state?.mission_id;
|
|
5
7
|
if (!missionId)
|
|
6
8
|
return inactive('mission_id_missing');
|
|
7
|
-
const capability = await readMadDbCapability(root, missionId);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const capability = explicitCapability || await readMadDbCapability(root, String(missionId));
|
|
10
|
+
const validation = validateCapabilityBinding(capability, state, classification);
|
|
11
|
+
if (!validation.ok)
|
|
12
|
+
return inactive(validation.reason);
|
|
13
|
+
if (isMadDbControlPlaneDeniedTool(classification.toolName || classification.tool_name))
|
|
14
|
+
return inactive('mad_db_control_plane_tool_denied');
|
|
15
|
+
if (!activeMadDbAllowsSqlPlane(classification))
|
|
16
|
+
return inactive('not_a_database_sql_plane_mutation');
|
|
12
17
|
return {
|
|
13
18
|
schema: MAD_DB_POLICY_DECISION_SCHEMA,
|
|
14
19
|
allowed: true,
|
|
15
20
|
action: 'allow',
|
|
16
|
-
mode: 'mad-db-
|
|
21
|
+
mode: 'mad-db-sql-plane-active',
|
|
17
22
|
priority: 0,
|
|
18
23
|
priority_order: ['mad-db', 'mad-sks', 'sealed-contract', 'default-db-safety'],
|
|
19
|
-
reasons: ['
|
|
24
|
+
reasons: ['mad_db_capability_v2_bound_sql_plane_authorized'],
|
|
20
25
|
audit_required: true,
|
|
21
|
-
mission_id:
|
|
26
|
+
mission_id: capability.mission_id,
|
|
22
27
|
cycle_id: capability.cycle_id,
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
runtime_session_id: capability.runtime_session_id,
|
|
29
|
+
project_ref_hash: hashRef(capability.project_ref),
|
|
30
|
+
operation_classes: madDbOperationClassesFromClassification(classification),
|
|
31
|
+
counters: capability.counters,
|
|
25
32
|
capability
|
|
26
33
|
};
|
|
27
34
|
}
|
|
35
|
+
export function validateCapabilityBinding(capability, state = {}, classification = {}) {
|
|
36
|
+
if (!capability)
|
|
37
|
+
return { ok: false, reason: 'mad_db_capability_missing' };
|
|
38
|
+
if (!isMadDbCapabilityActive(capability))
|
|
39
|
+
return { ok: false, reason: `mad_db_capability_${capability.status || 'inactive'}` };
|
|
40
|
+
if (!capability.project_ref)
|
|
41
|
+
return { ok: false, reason: 'mad_db_project_ref_missing' };
|
|
42
|
+
if (state?.mission_id && String(state.mission_id) !== capability.mission_id)
|
|
43
|
+
return { ok: false, reason: 'mad_db_mission_binding_mismatch' };
|
|
44
|
+
if (state?.mad_db_cycle_id && String(state.mad_db_cycle_id) !== capability.cycle_id)
|
|
45
|
+
return { ok: false, reason: 'mad_db_cycle_binding_mismatch' };
|
|
46
|
+
if (state?.mad_db_runtime_session_id && String(state.mad_db_runtime_session_id) !== capability.runtime_session_id)
|
|
47
|
+
return { ok: false, reason: 'mad_db_runtime_session_binding_mismatch' };
|
|
48
|
+
if (state?.mad_db_profile_sha256 && String(state.mad_db_profile_sha256) !== capability.transport.profile_sha256)
|
|
49
|
+
return { ok: false, reason: 'mad_db_profile_hash_mismatch' };
|
|
50
|
+
if (classification.toolReasons?.includes?.('dangerous_supabase_management_tool'))
|
|
51
|
+
return { ok: false, reason: 'mad_db_control_plane_tool_denied' };
|
|
52
|
+
return { ok: true, reason: 'ok' };
|
|
53
|
+
}
|
|
28
54
|
function inactive(reason) {
|
|
29
55
|
return {
|
|
30
56
|
schema: MAD_DB_POLICY_DECISION_SCHEMA,
|
|
@@ -36,13 +62,7 @@ function inactive(reason) {
|
|
|
36
62
|
audit_required: false
|
|
37
63
|
};
|
|
38
64
|
}
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
return true;
|
|
42
|
-
if (classification.toolReasons?.includes?.('database_tool'))
|
|
43
|
-
return true;
|
|
44
|
-
if (classification.toolReasons?.includes?.('migration_apply_tool'))
|
|
45
|
-
return true;
|
|
46
|
-
return false;
|
|
65
|
+
function hashRef(projectRef) {
|
|
66
|
+
return sha256(projectRef).slice(0, 16);
|
|
47
67
|
}
|
|
48
68
|
//# sourceMappingURL=mad-db-policy-resolver.js.map
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
export const MAD_DB_POLICY_SCHEMA = 'sks.mad-db-policy.v2';
|
|
2
|
+
export const MAD_DB_OPERATION_CLASSES = [
|
|
3
|
+
'create',
|
|
4
|
+
'alter',
|
|
5
|
+
'drop',
|
|
6
|
+
'drop_database_sql',
|
|
7
|
+
'insert',
|
|
8
|
+
'update',
|
|
9
|
+
'delete',
|
|
10
|
+
'all_row_update',
|
|
11
|
+
'all_row_delete',
|
|
12
|
+
'truncate',
|
|
13
|
+
'migration_apply',
|
|
14
|
+
'direct_execute_sql',
|
|
15
|
+
'rls_policy_change',
|
|
16
|
+
'function_or_trigger_change',
|
|
17
|
+
'index_change',
|
|
18
|
+
'unknown_sql_mutation'
|
|
19
|
+
];
|
|
20
|
+
export const MAD_DB_SQL_PLANE_TOOL_NAMES = Object.freeze([
|
|
21
|
+
'execute_sql',
|
|
22
|
+
'apply_migration',
|
|
23
|
+
'supabase.execute_sql',
|
|
24
|
+
'supabase.apply_migration',
|
|
25
|
+
'mcp__supabase__execute_sql',
|
|
26
|
+
'mcp__supabase__apply_migration',
|
|
27
|
+
'supabase_mad_db.execute_sql',
|
|
28
|
+
'supabase_mad_db.apply_migration',
|
|
29
|
+
'mcp__supabase_mad_db__execute_sql',
|
|
30
|
+
'mcp__supabase_mad_db__apply_migration'
|
|
31
|
+
]);
|
|
32
|
+
export const MAD_DB_CONTROL_PLANE_DENIED_TOOL_PATTERNS = Object.freeze([
|
|
33
|
+
'delete_project',
|
|
34
|
+
'pause_project',
|
|
35
|
+
'restore_project',
|
|
36
|
+
'create_project',
|
|
37
|
+
'list_organizations',
|
|
38
|
+
'get_organization',
|
|
39
|
+
'billing',
|
|
40
|
+
'organization',
|
|
41
|
+
'credential',
|
|
42
|
+
'access_token',
|
|
43
|
+
'service_role',
|
|
44
|
+
'delete_branch',
|
|
45
|
+
'reset_branch',
|
|
46
|
+
'merge_branch'
|
|
47
|
+
]);
|
|
48
|
+
export const MAD_DB_POLICY = Object.freeze({
|
|
49
|
+
schema: MAD_DB_POLICY_SCHEMA,
|
|
50
|
+
default_mode: 'deny_mutations',
|
|
51
|
+
active_mode: {
|
|
52
|
+
sql_plane: 'allow_all_mutations',
|
|
53
|
+
control_plane: 'deny',
|
|
54
|
+
requires: [
|
|
55
|
+
'capability_v2',
|
|
56
|
+
'project_binding',
|
|
57
|
+
'session_binding',
|
|
58
|
+
'write_transport_ready',
|
|
59
|
+
'not_expired'
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
sql_plane_allowed: MAD_DB_OPERATION_CLASSES,
|
|
63
|
+
sql_plane_tools: MAD_DB_SQL_PLANE_TOOL_NAMES,
|
|
64
|
+
control_plane_denied: MAD_DB_CONTROL_PLANE_DENIED_TOOL_PATTERNS,
|
|
65
|
+
normal_supabase_mcp: {
|
|
66
|
+
read_only_required: true,
|
|
67
|
+
project_ref_required: true
|
|
68
|
+
},
|
|
69
|
+
runtime_profile: {
|
|
70
|
+
mission_local_only: true,
|
|
71
|
+
read_only_omitted_only_for_active_capability: true,
|
|
72
|
+
features: ['database']
|
|
73
|
+
},
|
|
74
|
+
ttl: {
|
|
75
|
+
default_ms: 15 * 60 * 1000,
|
|
76
|
+
hard_max_ms: 30 * 60 * 1000
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
export function madDbPolicySnapshot() {
|
|
80
|
+
return MAD_DB_POLICY;
|
|
81
|
+
}
|
|
82
|
+
export function isMadDbSqlPlaneToolName(toolName) {
|
|
83
|
+
const normalized = normalizeToolName(toolName);
|
|
84
|
+
if (!normalized)
|
|
85
|
+
return false;
|
|
86
|
+
return MAD_DB_SQL_PLANE_TOOL_NAMES.some((name) => normalized.endsWith(normalizeToolName(name)) || normalized.includes(normalizeToolName(name)));
|
|
87
|
+
}
|
|
88
|
+
export function isMadDbControlPlaneDeniedTool(toolName) {
|
|
89
|
+
const normalized = normalizeToolName(toolName);
|
|
90
|
+
if (!normalized)
|
|
91
|
+
return false;
|
|
92
|
+
return MAD_DB_CONTROL_PLANE_DENIED_TOOL_PATTERNS.some((pattern) => normalized.includes(normalizeToolName(pattern)));
|
|
93
|
+
}
|
|
94
|
+
export function normalizeToolName(toolName) {
|
|
95
|
+
return String(toolName || '')
|
|
96
|
+
.trim()
|
|
97
|
+
.toLowerCase()
|
|
98
|
+
.replace(/[^a-z0-9_.-]+/g, '_');
|
|
99
|
+
}
|
|
100
|
+
export function madDbOperationClassesFromClassification(classification = {}) {
|
|
101
|
+
const reasons = new Set([
|
|
102
|
+
...stringArray(classification.reasons),
|
|
103
|
+
...stringArray(classification.sql?.reasons),
|
|
104
|
+
...stringArray(classification.command?.reasons),
|
|
105
|
+
...stringArray(classification.toolReasons)
|
|
106
|
+
]);
|
|
107
|
+
const out = new Set();
|
|
108
|
+
const toolName = classification.toolName || classification.tool_name || '';
|
|
109
|
+
if (isMadDbSqlPlaneToolName(toolName))
|
|
110
|
+
out.add('direct_execute_sql');
|
|
111
|
+
if (reasons.has('migration_apply_tool') || reasons.has('supabase_migration_apply'))
|
|
112
|
+
out.add('migration_apply');
|
|
113
|
+
if (hasAny(reasons, ['drop_database']))
|
|
114
|
+
out.add('drop_database_sql');
|
|
115
|
+
if (hasAny(reasons, ['drop_schema', 'drop_table', 'drop_view', 'drop_materialized_view', 'drop_extension', 'drop_policy', 'drop_statement', 'alter_table_drop']))
|
|
116
|
+
out.add('drop');
|
|
117
|
+
if (hasAny(reasons, ['truncate']))
|
|
118
|
+
out.add('truncate');
|
|
119
|
+
if (hasAny(reasons, ['insert_or_upsert']))
|
|
120
|
+
out.add('insert');
|
|
121
|
+
if (hasAny(reasons, ['update_with_where']))
|
|
122
|
+
out.add('update');
|
|
123
|
+
if (hasAny(reasons, ['update_without_where']))
|
|
124
|
+
out.add('all_row_update');
|
|
125
|
+
if (hasAny(reasons, ['delete_with_where']))
|
|
126
|
+
out.add('delete');
|
|
127
|
+
if (hasAny(reasons, ['delete_without_where']))
|
|
128
|
+
out.add('all_row_delete');
|
|
129
|
+
if (hasAny(reasons, ['schema_change', 'alter_table_rename']))
|
|
130
|
+
out.add('alter');
|
|
131
|
+
if (hasAny(reasons, ['create_or_replace']))
|
|
132
|
+
out.add('function_or_trigger_change');
|
|
133
|
+
const statements = stringArray(classification.sql?.statements);
|
|
134
|
+
for (const statement of statements) {
|
|
135
|
+
const normalized = statement.trim().toLowerCase();
|
|
136
|
+
if (/^create\s+(table|schema|view|materialized\s+view)/.test(normalized))
|
|
137
|
+
out.add('create');
|
|
138
|
+
if (/^create\s+(index|unique\s+index)/.test(normalized))
|
|
139
|
+
out.add('index_change');
|
|
140
|
+
if (/^drop\s+index/.test(normalized))
|
|
141
|
+
out.add('index_change');
|
|
142
|
+
if (/policy|row\s+level\s+security|rls/.test(normalized))
|
|
143
|
+
out.add('rls_policy_change');
|
|
144
|
+
if (/function|trigger|procedure/.test(normalized))
|
|
145
|
+
out.add('function_or_trigger_change');
|
|
146
|
+
}
|
|
147
|
+
if (!out.size && ['write', 'destructive', 'possible_db'].includes(String(classification.level || '')))
|
|
148
|
+
out.add('unknown_sql_mutation');
|
|
149
|
+
return [...out];
|
|
150
|
+
}
|
|
151
|
+
export function activeMadDbAllowsSqlPlane(classification = {}) {
|
|
152
|
+
const toolName = classification.toolName || classification.tool_name || '';
|
|
153
|
+
if (isMadDbControlPlaneDeniedTool(toolName))
|
|
154
|
+
return false;
|
|
155
|
+
if (classification.toolReasons?.includes?.('dangerous_supabase_management_tool'))
|
|
156
|
+
return false;
|
|
157
|
+
if (isMadDbSqlPlaneToolName(toolName))
|
|
158
|
+
return true;
|
|
159
|
+
return ['write', 'destructive', 'possible_db'].includes(String(classification.level || ''));
|
|
160
|
+
}
|
|
161
|
+
export function madDbSkillText(commandPrefix = 'sks') {
|
|
162
|
+
return `---
|
|
163
|
+
name: mad-db
|
|
164
|
+
description: First-class MadDB SQL-plane execution route for explicit $MAD-DB and ${commandPrefix} mad-db run|exec|apply-migration.
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
Use only when the operator explicitly invokes $MAD-DB/$mad-db or ${commandPrefix} mad-db run|exec|apply-migration. This is the single approval boundary for the active MadDB cycle: execute SQL-plane mutations that the operator requested, including CREATE, ALTER, table/schema DROP, column add/drop/rename, INSERT, UPDATE, DELETE including all-row mutations, TRUNCATE, execute_sql, and apply_migration. Do not ask again for each DROP/TRUNCATE/all-row DELETE inside the same bound cycle.
|
|
168
|
+
|
|
169
|
+
Keep normal Supabase MCP configuration read-only. MadDB must create a mission-local ephemeral write-capable Supabase MCP profile bound to capability v2, project_ref, root, mission, thread/session, intent, runtime profile hash, TTL, and SQL-plane operation classes. Verify execute_sql and apply_migration availability before claiming readiness. Require actual tool results plus independent read-back verification before claiming success. Close/revoke the capability and runtime profile in finally and prove read-only restoration.
|
|
170
|
+
|
|
171
|
+
Still deny Supabase account/project/billing/credential control-plane actions, credential exfiltration, unrelated storage/edge-function/admin changes, persistent security weakening, and unrequested fallback implementation. Pair with db-safety-guard, Context7 evidence for MCP/API docs, route-local reflection, and Honest Mode.`;
|
|
172
|
+
}
|
|
173
|
+
export function dbSafetyGuardSkillText() {
|
|
174
|
+
return `---
|
|
175
|
+
name: db-safety-guard
|
|
176
|
+
description: Enforce Sneakoscope Codex database safety before using SQL, Supabase MCP, Postgres, Prisma, Drizzle, Knex, or migration commands.
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
Rules:
|
|
180
|
+
- Default mode is read-only: do not run DROP, TRUNCATE, mass DELETE/UPDATE, db reset, db push, project deletion, branch reset/merge/delete, or RLS-disabling operations.
|
|
181
|
+
- Supabase MCP must be read-only and project-scoped by default.
|
|
182
|
+
- Live execute_sql writes are blocked unless a bound active MadDB capability v2 is present.
|
|
183
|
+
- Active MadDB is the explicit exception: SQL-plane mutations requested by $MAD-DB or sks mad-db run|exec|apply-migration are allowed and must be executed with read-back verification.
|
|
184
|
+
- Supabase project/account/billing/credential control-plane actions remain denied even in MadDB.
|
|
185
|
+
- If no active bound MadDB cycle exists, fall back to read-only only.`;
|
|
186
|
+
}
|
|
187
|
+
function stringArray(value) {
|
|
188
|
+
if (!Array.isArray(value))
|
|
189
|
+
return [];
|
|
190
|
+
return value.map((entry) => String(entry || '')).filter(Boolean);
|
|
191
|
+
}
|
|
192
|
+
function hasAny(values, keys) {
|
|
193
|
+
return keys.some((key) => values.has(key));
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=mad-db-policy.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { nowIso, sha256, writeJsonAtomic } from '../fsx.js';
|
|
3
|
+
import { missionDir } from '../mission.js';
|
|
4
|
+
export async function runReadBackChecks(input) {
|
|
5
|
+
const rows = [];
|
|
6
|
+
for (const check of input.checks) {
|
|
7
|
+
const result = await input.executor.executeSql(check.query);
|
|
8
|
+
rows.push({
|
|
9
|
+
id: check.id,
|
|
10
|
+
query_sha256: sha256(check.query),
|
|
11
|
+
ok: result.ok === (check.expectOk ?? true),
|
|
12
|
+
result_digest: result.result_digest,
|
|
13
|
+
row_count: result.row_count
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const proof = {
|
|
17
|
+
schema: 'sks.mad-db-read-back-proof.v1',
|
|
18
|
+
generated_at: nowIso(),
|
|
19
|
+
ok: rows.every((row) => row.ok),
|
|
20
|
+
checks: rows,
|
|
21
|
+
raw_rows_recorded: false
|
|
22
|
+
};
|
|
23
|
+
const proofPath = path.join(missionDir(input.root, input.missionId), 'mad-db', 'read-back-proof.json');
|
|
24
|
+
await writeJsonAtomic(proofPath, proof);
|
|
25
|
+
return { ...proof, proof_path: path.relative(input.root, proofPath).split(path.sep).join('/') };
|
|
26
|
+
}
|
|
27
|
+
export function readBackCheck(id, query) {
|
|
28
|
+
return { id, query, expectOk: true };
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=mad-db-postconditions.js.map
|