verify-capture-flywheel-live-smoke.mjs
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6
docs: record AIP-b SD-21 land (KN #308)
Human
10 days ago
| 1 | #!/usr/bin/env node |
| 2 | /** |
| 3 | * Live capture flywheel smoke (9-apply / 9-kn-c re-run driver). |
| 4 | * |
| 5 | * Exchanges the durable kt_agent_ credential for an access JWT, then drives the |
| 6 | * hosted capture path: POST observe (fresh candidate) → GET candidates (blob |
| 7 | * persistence check) → POST propose (canister proposal for the operator to |
| 8 | * approve in the Hub tray). Never prints secrets. Exit 0 only on full PASS. |
| 9 | * |
| 10 | * Content-minimized session meta only (ids / hashes / counts — no raw content), |
| 11 | * matching the FLOW-CAPTURE-FLYWHEEL contract. |
| 12 | * |
| 13 | * Usage: |
| 14 | * KNOWTATION_HUB_AGENT_CREDENTIAL_FILE=~/.config/knowtation/agent_cred \ |
| 15 | * node scripts/verify-capture-flywheel-live-smoke.mjs |
| 16 | * |
| 17 | * Optional env: |
| 18 | * KNOWTATION_HUB_URL (default https://api.knowtation.store) |
| 19 | * KNOWTATION_HUB_VAULT_ID (default: first vault_id from the token exchange) |
| 20 | * SMOKE_FORCE_NEW_FLOW=1 (bypass structural-overlap dedup with force_new_flow |
| 21 | * when a prior smoke Flow already exists) |
| 22 | */ |
| 23 | |
| 24 | import fs from 'node:fs'; |
| 25 | import path from 'node:path'; |
| 26 | import crypto from 'node:crypto'; |
| 27 | |
| 28 | const apiBase = (process.env.KNOWTATION_HUB_URL || 'https://api.knowtation.store').replace(/\/$/, ''); |
| 29 | |
| 30 | function resolveCredential() { |
| 31 | let c = (process.env.KNOWTATION_HUB_AGENT_CREDENTIAL || '').trim(); |
| 32 | const fp = (process.env.KNOWTATION_HUB_AGENT_CREDENTIAL_FILE || '').trim(); |
| 33 | if (!c && fp) { |
| 34 | const expanded = fp.startsWith('~') ? path.join(process.env.HOME || '', fp.slice(1)) : fp; |
| 35 | c = fs.readFileSync(expanded, 'utf8').trim(); |
| 36 | } |
| 37 | return c; |
| 38 | } |
| 39 | |
| 40 | function fail(step, detail) { |
| 41 | console.log(`FAIL ${step}: ${detail}`); |
| 42 | process.exit(1); |
| 43 | } |
| 44 | |
| 45 | async function jfetch(step, url, opts = {}) { |
| 46 | const res = await fetch(url, opts); |
| 47 | const text = await res.text(); |
| 48 | let json = {}; |
| 49 | try { |
| 50 | json = text ? JSON.parse(text) : {}; |
| 51 | } catch { |
| 52 | json = { raw: text.slice(0, 200) }; |
| 53 | } |
| 54 | return { step, res, json, text }; |
| 55 | } |
| 56 | |
| 57 | const credential = resolveCredential(); |
| 58 | if (!credential) fail('setup', 'set KNOWTATION_HUB_AGENT_CREDENTIAL or _FILE'); |
| 59 | if (!credential.startsWith('kt_agent_')) fail('setup', 'credential must start with kt_agent_'); |
| 60 | |
| 61 | console.log(`api=${apiBase}`); |
| 62 | |
| 63 | // 1. Exchange credential → access JWT. |
| 64 | const exch = await jfetch('exchange', `${apiBase}/api/v1/auth/agent/token`, { |
| 65 | method: 'POST', |
| 66 | headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, |
| 67 | body: JSON.stringify({ credential }), |
| 68 | }); |
| 69 | if (!exch.res.ok) fail('exchange', `HTTP ${exch.res.status} ${exch.json.code || ''} ${exch.json.error || ''}`); |
| 70 | const access = exch.json.access_token; |
| 71 | if (!access) fail('exchange', 'missing access_token'); |
| 72 | const vaultId = |
| 73 | process.env.KNOWTATION_HUB_VAULT_ID || |
| 74 | (Array.isArray(exch.json.vault_ids) ? exch.json.vault_ids[0] : '') || |
| 75 | 'default'; |
| 76 | console.log(`exchange OK scopes=${JSON.stringify(exch.json.scopes)} vault=${vaultId}`); |
| 77 | |
| 78 | const headers = { |
| 79 | 'Content-Type': 'application/json', |
| 80 | Accept: 'application/json', |
| 81 | Authorization: `Bearer ${access}`, |
| 82 | 'X-Vault-Id': vaultId, |
| 83 | }; |
| 84 | |
| 85 | // 2. Fresh observe — content-minimized meta with enough repetition to detect. |
| 86 | const sessionId = crypto.randomBytes(32).toString('hex'); |
| 87 | const observe = await jfetch('observe', `${apiBase}/api/v1/flows/capture/observe`, { |
| 88 | method: 'POST', |
| 89 | headers, |
| 90 | body: JSON.stringify({ |
| 91 | session_id: sessionId, |
| 92 | step_sequence_refs: ['flow_weekly_review#1', 'flow_weekly_review#2'], |
| 93 | skill_ref_ids: ['mcp_prompt:daily-brief'], |
| 94 | observed_counts: { repetition: 4, repeated_correction: 1 }, |
| 95 | signal_hints: ['repetition'], |
| 96 | harness: 'overseer-live-smoke', |
| 97 | }), |
| 98 | }); |
| 99 | if (!observe.res.ok) fail('observe', `HTTP ${observe.res.status} ${observe.json.code || ''} ${observe.json.error || ''}`); |
| 100 | if (observe.json.detection_authorized !== true) { |
| 101 | fail('observe', `detection_authorized=${observe.json.detection_authorized} (FLOW_CAPTURE_DETECTION_ENABLED off hosted-side?)`); |
| 102 | } |
| 103 | const candidate = (observe.json.candidates || [])[0]; |
| 104 | if (!candidate) fail('observe', 'no candidate returned'); |
| 105 | console.log(`observe OK candidate_id=${candidate.candidate_id} confidence=${candidate.confidence}`); |
| 106 | |
| 107 | // 3. Candidates list — with the blob-persistence fix the candidate must be |
| 108 | // visible regardless of which lambda instance serves this read. |
| 109 | const list = await jfetch('candidates', `${apiBase}/api/v1/flows/candidates?limit=50`, { headers }); |
| 110 | if (!list.res.ok) fail('candidates', `HTTP ${list.res.status} ${list.json.code || ''}`); |
| 111 | const seen = (list.json.candidates || []).some((c) => c.candidate_id === candidate.candidate_id); |
| 112 | if (!seen) fail('candidates', `candidate ${candidate.candidate_id} not in list — blob persistence NOT live`); |
| 113 | console.log(`candidates OK (${(list.json.candidates || []).length} listed, new candidate present)`); |
| 114 | |
| 115 | // 4. Propose — creates the canister proposal for the operator's Hub-tray approve. |
| 116 | const propose = await jfetch( |
| 117 | 'propose', |
| 118 | `${apiBase}/api/v1/flows/candidates/${encodeURIComponent(candidate.candidate_id)}/propose`, |
| 119 | { |
| 120 | method: 'POST', |
| 121 | headers, |
| 122 | body: JSON.stringify({ |
| 123 | confirmed_scope: 'personal', |
| 124 | intent: 'Promote captured weekly-review procedure to a saved Flow (9-apply re-run)', |
| 125 | ...(process.env.SMOKE_FORCE_NEW_FLOW === '1' ? { force_new_flow: true } : {}), |
| 126 | }), |
| 127 | }, |
| 128 | ); |
| 129 | if (!propose.res.ok) fail('propose', `HTTP ${propose.res.status} ${propose.json.code || ''} ${propose.json.error || ''}`); |
| 130 | console.log(`propose OK proposal_id=${propose.json.proposal_id}`); |
| 131 | |
| 132 | console.log(''); |
| 133 | console.log('PASS capture flywheel live smoke (exchange + observe + candidates + propose)'); |
| 134 | console.log(`NEXT: operator approves ${propose.json.proposal_id} in the Hub tray (https://knowtation.store/hub)`); |
| 135 | process.exit(0); |
File History
1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6
docs: record AIP-b SD-21 land (KN #308)
Human
10 days ago