verify-hosted-external-protocol-audit-smoke.mjs
sha256:baa800aedf841dbf32081aa7b2befa288ac33dfc7175ac55014c55c4d8c742f9
docs: move durable-auth freeze/evidence to local developmen…
Human
11 days ago
| 1 | #!/usr/bin/env node |
| 2 | /** |
| 3 | * Phase 7D-L2 — hosted external agent protocol complete + audit-append smoke (spec §16.2 C1–C6). |
| 4 | * |
| 5 | * Prerequisites: |
| 6 | * - Bridge: EXTERNAL_PROTOCOL_AUTHORIZED=true, DELEGATION_ENABLED=1 (Tier 3 flips) |
| 7 | * - Scooling local: SCOOLING_EXTERNAL_PROTOCOL_AUDIT_APPEND=enabled (operator) |
| 8 | * - Hub JWT with vault access (admin can approve proposals) |
| 9 | * |
| 10 | * Usage: |
| 11 | * KNOWTATION_HUB_TOKEN='<jwt>' KNOWTATION_HUB_VAULT_ID=default \ |
| 12 | * node scripts/verify-hosted-external-protocol-audit-smoke.mjs |
| 13 | * |
| 14 | * Optional: |
| 15 | * KNOWTATION_HUB_API=https://api.knowtation.store |
| 16 | * EXTERNAL_PROTOCOL_SMOKE_AGENT_ID=agent_7d_smoke01 |
| 17 | */ |
| 18 | |
| 19 | import fs from 'node:fs'; |
| 20 | import path from 'node:path'; |
| 21 | import { fileURLToPath } from 'node:url'; |
| 22 | import dotenv from 'dotenv'; |
| 23 | import { taskStateId } from '../lib/task/task-write.mjs'; |
| 24 | |
| 25 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 26 | const repoRoot = path.resolve(__dirname, '..'); |
| 27 | dotenv.config({ path: path.join(repoRoot, '.env') }); |
| 28 | |
| 29 | const apiBase = (process.env.KNOWTATION_HUB_API || process.env.KNOWTATION_HUB_URL || 'https://api.knowtation.store').replace(/\/$/, ''); |
| 30 | const vaultId = process.env.KNOWTATION_HUB_VAULT_ID || 'default'; |
| 31 | const agentId = (process.env.EXTERNAL_PROTOCOL_SMOKE_AGENT_ID || 'agent_7d_smoke01').trim(); |
| 32 | /** L1 smoke task — present in external-protocol flow-store blob on hosted bridge. */ |
| 33 | const TASK_L1 = (process.env.EXTERNAL_PROTOCOL_SMOKE_TASK_ID || 'task_7d_smoke_hello').trim(); |
| 34 | const TASK_C2 = process.env.EXTERNAL_PROTOCOL_SMOKE_TASK_C2 || TASK_L1; |
| 35 | const TASK_C4 = process.env.EXTERNAL_PROTOCOL_SMOKE_TASK_C4 || TASK_L1; |
| 36 | const TASK_C5 = process.env.EXTERNAL_PROTOCOL_SMOKE_TASK_C5 || TASK_L1; |
| 37 | const TASK_C6 = process.env.EXTERNAL_PROTOCOL_SMOKE_TASK_C6 || TASK_L1; |
| 38 | const SMOKE_TIMEOUT_MS = 25_000; |
| 39 | |
| 40 | /** @type {{ step: string, ok: boolean, detail: string }[]} */ |
| 41 | const results = []; |
| 42 | |
| 43 | function resolveToken() { |
| 44 | let token = process.env.KNOWTATION_HUB_TOKEN || process.env.KNOWTATION_AUTH_TOKEN || process.env.HUB_JWT || ''; |
| 45 | const fp = (process.env.KNOWTATION_HUB_TOKEN_FILE || '').trim(); |
| 46 | if (!token && fp) { |
| 47 | const expanded = fp.startsWith('~') ? path.join(process.env.HOME || '', fp.slice(1)) : fp; |
| 48 | token = fs.readFileSync(expanded, 'utf8').trim(); |
| 49 | } |
| 50 | return token; |
| 51 | } |
| 52 | |
| 53 | function record(step, ok, detail) { |
| 54 | results.push({ step, ok, detail }); |
| 55 | console.log(`[${ok ? 'PASS' : 'FAIL'}] ${step}: ${detail}`); |
| 56 | } |
| 57 | |
| 58 | function hubHeaders(extra = {}) { |
| 59 | const token = resolveToken(); |
| 60 | const h = { |
| 61 | Accept: 'application/json', |
| 62 | 'Content-Type': 'application/json', |
| 63 | 'X-Vault-Id': vaultId, |
| 64 | ...extra, |
| 65 | }; |
| 66 | if (token) h.Authorization = `Bearer ${token}`; |
| 67 | return h; |
| 68 | } |
| 69 | |
| 70 | async function api(method, pathSuffix, body, extraHeaders = {}) { |
| 71 | const res = await fetch(`${apiBase}${pathSuffix}`, { |
| 72 | method, |
| 73 | headers: hubHeaders(extraHeaders), |
| 74 | body: body ? JSON.stringify(body) : undefined, |
| 75 | signal: AbortSignal.timeout(SMOKE_TIMEOUT_MS), |
| 76 | }); |
| 77 | const text = await res.text(); |
| 78 | let json = null; |
| 79 | try { |
| 80 | json = text ? JSON.parse(text) : null; |
| 81 | } catch { |
| 82 | json = { raw: text.slice(0, 200) }; |
| 83 | } |
| 84 | return { status: res.status, json }; |
| 85 | } |
| 86 | |
| 87 | async function apiWithDelegation(method, pathSuffix, body, bearer) { |
| 88 | return api(method, pathSuffix, body, { 'X-Delegation-Bearer': bearer }); |
| 89 | } |
| 90 | |
| 91 | async function approveAndApplyProposal(proposalId) { |
| 92 | const approve = await api('POST', `/api/v1/proposals/${encodeURIComponent(proposalId)}/approve`); |
| 93 | if (approve.status < 200 || approve.status >= 300) { |
| 94 | return { ok: false, detail: `approve HTTP ${approve.status} ${approve.json?.code ?? ''}` }; |
| 95 | } |
| 96 | const apply = await api( |
| 97 | 'POST', |
| 98 | `/api/v1/delegation/proposals/${encodeURIComponent(proposalId)}/apply-approved`, |
| 99 | {}, |
| 100 | ); |
| 101 | if (apply.status < 200 || apply.status >= 300) { |
| 102 | return { ok: false, detail: `apply-approved HTTP ${apply.status} ${apply.json?.code ?? ''}` }; |
| 103 | } |
| 104 | return { ok: true, detail: `proposal ${proposalId} applied` }; |
| 105 | } |
| 106 | |
| 107 | async function ensureExternalProviderIdentity() { |
| 108 | const list = await api('GET', `/api/v1/agents/identities?kind=external_provider&status=active`); |
| 109 | if (list.status !== 200) { |
| 110 | return { ok: false, detail: `identity list HTTP ${list.status}` }; |
| 111 | } |
| 112 | const found = list.json?.identities?.find((entry) => entry.agent_id === agentId); |
| 113 | if (found) { |
| 114 | return { ok: true, detail: `existing ${agentId}` }; |
| 115 | } |
| 116 | const register = await api('POST', '/api/v1/agents/identities', { |
| 117 | kind: 'external_provider', |
| 118 | agent_id: agentId, |
| 119 | label: '7D-L2 smoke external provider', |
| 120 | scope_ceiling: 'personal', |
| 121 | }); |
| 122 | if (register.status !== 201 || !register.json?.proposal_id) { |
| 123 | return { |
| 124 | ok: false, |
| 125 | detail: `register HTTP ${register.status} ${register.json?.code ?? register.json?.error ?? ''}`, |
| 126 | }; |
| 127 | } |
| 128 | const applied = await approveAndApplyProposal(register.json.proposal_id); |
| 129 | if (!applied.ok) return applied; |
| 130 | return { ok: true, detail: `registered ${agentId}` }; |
| 131 | } |
| 132 | |
| 133 | async function ensureConsent(taskIds) { |
| 134 | const consentBody = { |
| 135 | delegate_agent_id: agentId, |
| 136 | scope: 'personal', |
| 137 | allowed_task_ids: taskIds, |
| 138 | }; |
| 139 | const propose = await api('POST', '/api/v1/delegation/consents', consentBody); |
| 140 | if (propose.status !== 201 || !propose.json?.proposal_id) { |
| 141 | return { |
| 142 | ok: false, |
| 143 | consentId: null, |
| 144 | detail: `consent propose HTTP ${propose.status} ${propose.json?.code ?? ''}`, |
| 145 | }; |
| 146 | } |
| 147 | const applied = await approveAndApplyProposal(propose.json.proposal_id); |
| 148 | if (!applied.ok) return { ok: false, consentId: null, detail: applied.detail }; |
| 149 | const consentId = propose.json.consent_id || propose.json.proposal_id; |
| 150 | return { ok: true, consentId, detail: applied.detail }; |
| 151 | } |
| 152 | |
| 153 | async function ensureSmokeTask(taskId, title, boundaryPolicy) { |
| 154 | const get = await api('GET', `/api/v1/tasks/${encodeURIComponent(taskId)}`); |
| 155 | if (get.status === 200 && get.json?.task) { |
| 156 | const bp = get.json.task.boundary_policy ?? 'observe_only'; |
| 157 | return { |
| 158 | ok: true, |
| 159 | detail: `task ${taskId} exists status=${get.json.task.status} boundary=${bp}`, |
| 160 | boundaryPolicy: bp, |
| 161 | }; |
| 162 | } |
| 163 | |
| 164 | const propose = await api('POST', '/api/v1/tasks/proposals', { |
| 165 | proposal_kind: 'task_create', |
| 166 | intent: `7D-L2 smoke task — ${title}`, |
| 167 | task: { |
| 168 | task_id: taskId, |
| 169 | kind: 'personal', |
| 170 | scope: 'personal', |
| 171 | title, |
| 172 | workspace_id: 'ws-personal', |
| 173 | due_at: null, |
| 174 | assignee_ref: agentId, |
| 175 | assigner_ref: null, |
| 176 | artifact_links: [], |
| 177 | boundary_policy: boundaryPolicy, |
| 178 | }, |
| 179 | }); |
| 180 | if (propose.status !== 201 || !propose.json?.proposal_id) { |
| 181 | return { |
| 182 | ok: false, |
| 183 | detail: `task propose HTTP ${propose.status} ${propose.json?.code ?? propose.json?.error ?? ''}`, |
| 184 | boundaryPolicy: null, |
| 185 | }; |
| 186 | } |
| 187 | const approve = await api('POST', `/api/v1/proposals/${encodeURIComponent(propose.json.proposal_id)}/approve`); |
| 188 | if (approve.status < 200 || approve.status >= 300) { |
| 189 | return { ok: false, detail: `task approve HTTP ${approve.status}`, boundaryPolicy: null }; |
| 190 | } |
| 191 | if (approve.json?.task_index_applied !== true) { |
| 192 | const apply = await api( |
| 193 | 'POST', |
| 194 | `/api/v1/tasks/proposals/${encodeURIComponent(propose.json.proposal_id)}/apply-approved`, |
| 195 | {}, |
| 196 | ); |
| 197 | if (apply.status < 200 || apply.status >= 300) { |
| 198 | return { |
| 199 | ok: false, |
| 200 | detail: `task apply-approved HTTP ${apply.status} ${apply.json?.code ?? ''}`, |
| 201 | boundaryPolicy: null, |
| 202 | }; |
| 203 | } |
| 204 | } |
| 205 | const verify = await api('GET', `/api/v1/tasks/${encodeURIComponent(taskId)}`); |
| 206 | const applied = |
| 207 | approve.json?.task_index_applied === true || |
| 208 | (verify.status === 200 && verify.json?.task?.task_id === taskId); |
| 209 | if (!applied) { |
| 210 | return { |
| 211 | ok: false, |
| 212 | detail: `task index not applied: ${approve.json?.task_apply_error ?? verify.json?.code ?? 'unknown'}`, |
| 213 | boundaryPolicy: null, |
| 214 | }; |
| 215 | } |
| 216 | const bp = verify.json?.task?.boundary_policy ?? boundaryPolicy; |
| 217 | return { ok: true, detail: `created task ${taskId} boundary=${bp}`, boundaryPolicy: bp }; |
| 218 | } |
| 219 | |
| 220 | async function discoverPendingSmokeTask() { |
| 221 | const list = await api('GET', '/api/v1/tasks?status=pending'); |
| 222 | if (list.status !== 200 || !Array.isArray(list.json?.tasks)) { |
| 223 | return { ok: false, taskId: null, detail: `task list HTTP ${list.status}` }; |
| 224 | } |
| 225 | const match = |
| 226 | list.json.tasks.find((t) => t.assignee_ref === agentId && t.status === 'pending') || |
| 227 | list.json.tasks.find((t) => t.assignee_ref === agentId) || |
| 228 | list.json.tasks.find((t) => t.status === 'pending' && (t.assignee_ref === agentId || t.assignee_ref === '*')); |
| 229 | if (!match?.task_id) { |
| 230 | return { |
| 231 | ok: false, |
| 232 | taskId: null, |
| 233 | detail: `no pending task for ${agentId} — cancel/recreate after bridge deploy`, |
| 234 | }; |
| 235 | } |
| 236 | return { ok: true, taskId: match.task_id, detail: `discovered ${match.task_id} status=${match.status}` }; |
| 237 | } |
| 238 | |
| 239 | async function resetTaskToPending(taskId) { |
| 240 | const get = await api('GET', `/api/v1/tasks/${encodeURIComponent(taskId)}`); |
| 241 | if (get.status !== 200 || !get.json?.task) { |
| 242 | return { ok: false, detail: `GET task HTTP ${get.status}` }; |
| 243 | } |
| 244 | const task = get.json.task; |
| 245 | if (task.status === 'pending') { |
| 246 | return { ok: true, detail: 'already pending' }; |
| 247 | } |
| 248 | if (task.status === 'blocked') { |
| 249 | // blocked → pending is not a valid task-store transition; external claim needs pending |
| 250 | // or blocked+principal_cleared. Skip reset — claim may return task_not_claimable until |
| 251 | // bridge flow-store blob sync is deployed and task is re-created pending. |
| 252 | return { ok: true, detail: 'blocked (skip reset — redeploy bridge or recreate task pending)' }; |
| 253 | } |
| 254 | const baseStateId = taskStateId(task); |
| 255 | if (!baseStateId) { |
| 256 | return { ok: false, detail: 'missing state_id for status update' }; |
| 257 | } |
| 258 | const propose = await api('POST', '/api/v1/tasks/proposals', { |
| 259 | proposal_kind: 'task_status_update', |
| 260 | intent: '7D-L2 smoke reset task to pending', |
| 261 | task_id: taskId, |
| 262 | base_state_id: baseStateId, |
| 263 | status: 'pending', |
| 264 | skip_reason: null, |
| 265 | }); |
| 266 | if (propose.status !== 201 || !propose.json?.proposal_id) { |
| 267 | return { |
| 268 | ok: false, |
| 269 | detail: `status propose HTTP ${propose.status} ${propose.json?.code ?? propose.json?.error ?? ''}`, |
| 270 | }; |
| 271 | } |
| 272 | const approve = await api('POST', `/api/v1/proposals/${encodeURIComponent(propose.json.proposal_id)}/approve`); |
| 273 | if (approve.status < 200 || approve.status >= 300) { |
| 274 | return { ok: false, detail: `status approve HTTP ${approve.status}` }; |
| 275 | } |
| 276 | if (approve.json?.task_index_applied !== true) { |
| 277 | const apply = await api( |
| 278 | 'POST', |
| 279 | `/api/v1/tasks/proposals/${encodeURIComponent(propose.json.proposal_id)}/apply-approved`, |
| 280 | {}, |
| 281 | ); |
| 282 | if (apply.status < 200 || apply.status >= 300) { |
| 283 | return { ok: false, detail: `status apply HTTP ${apply.status}` }; |
| 284 | } |
| 285 | } |
| 286 | return { ok: true, detail: `reset ${taskId} → pending` }; |
| 287 | } |
| 288 | |
| 289 | async function prepareClaimableTask(taskId, title, boundaryPolicy) { |
| 290 | const ensured = await ensureSmokeTask(taskId, title, boundaryPolicy); |
| 291 | if (!ensured.ok) return ensured; |
| 292 | const reset = await resetTaskToPending(taskId); |
| 293 | if (!reset.ok) return reset; |
| 294 | const probe = await api('GET', `/api/v1/tasks/${encodeURIComponent(taskId)}`); |
| 295 | const bp = probe.json?.task?.boundary_policy ?? ensured.boundaryPolicy ?? 'observe_only'; |
| 296 | return { ok: true, detail: `${ensured.detail}; ${reset.detail}`, boundaryPolicy: bp }; |
| 297 | } |
| 298 | |
| 299 | async function mintGrant(consentId, taskRef) { |
| 300 | const mint = await api('POST', '/api/v1/delegation/grants', { |
| 301 | consent_id: consentId, |
| 302 | actor_agent_id: agentId, |
| 303 | task_ref: taskRef, |
| 304 | ttl_seconds: 3600, |
| 305 | }); |
| 306 | if (mint.status !== 201 || !mint.json?.bearer) { |
| 307 | return { ok: false, bearer: null, grantId: null, detail: `mint HTTP ${mint.status} ${mint.json?.code ?? ''}` }; |
| 308 | } |
| 309 | return { |
| 310 | ok: true, |
| 311 | bearer: mint.json.bearer, |
| 312 | grantId: mint.json.grant?.grant_id ?? null, |
| 313 | detail: `grant ${mint.json.grant?.grant_id ?? 'minted'}`, |
| 314 | }; |
| 315 | } |
| 316 | |
| 317 | async function grantActionCount(bearer, grantId) { |
| 318 | const list = await apiWithDelegation( |
| 319 | 'GET', |
| 320 | `/api/v1/delegation/grants?actor_agent_id=${encodeURIComponent(agentId)}`, |
| 321 | undefined, |
| 322 | bearer, |
| 323 | ); |
| 324 | if (list.status !== 200 || !Array.isArray(list.json?.grants)) { |
| 325 | return null; |
| 326 | } |
| 327 | const grant = list.json.grants.find((g) => g.grant_id === grantId); |
| 328 | return typeof grant?.action_count === 'number' ? grant.action_count : null; |
| 329 | } |
| 330 | |
| 331 | async function claimTask(bearer, taskId, idempotencyKey, leaseTtlSeconds = 900) { |
| 332 | return apiWithDelegation( |
| 333 | 'POST', |
| 334 | `/api/v1/agent-protocol/tasks/${encodeURIComponent(taskId)}/claim`, |
| 335 | { idempotency_key: idempotencyKey, lease_ttl_seconds: leaseTtlSeconds }, |
| 336 | bearer, |
| 337 | ); |
| 338 | } |
| 339 | |
| 340 | async function completeTask(bearer, taskId, passId, idempotencyKey, outcome, evidenceRefs = []) { |
| 341 | return apiWithDelegation( |
| 342 | 'POST', |
| 343 | `/api/v1/agent-protocol/tasks/${encodeURIComponent(taskId)}/complete`, |
| 344 | { |
| 345 | pass_id: passId, |
| 346 | idempotency_key: idempotencyKey, |
| 347 | outcome, |
| 348 | receipt_md: '7D-L2 smoke receipt.', |
| 349 | evidence_refs: evidenceRefs, |
| 350 | }, |
| 351 | bearer, |
| 352 | ); |
| 353 | } |
| 354 | |
| 355 | function sleep(ms) { |
| 356 | return new Promise((resolve) => setTimeout(resolve, ms)); |
| 357 | } |
| 358 | |
| 359 | async function main() { |
| 360 | console.log(`7D-L2 external protocol audit smoke — ${apiBase} vault=${vaultId}`); |
| 361 | |
| 362 | const scoolingAuditAppend = |
| 363 | process.env.SCOOLING_EXTERNAL_PROTOCOL_AUDIT_APPEND === 'enabled' || |
| 364 | process.env.EXTERNAL_PROTOCOL_AUDIT_SMOKE === 'enabled'; |
| 365 | |
| 366 | if (!resolveToken()) { |
| 367 | record('auth', false, 'KNOWTATION_HUB_TOKEN required'); |
| 368 | process.exit(1); |
| 369 | } |
| 370 | |
| 371 | const session = await api('GET', '/api/v1/auth/session'); |
| 372 | if (session.status !== 200) { |
| 373 | record('auth', false, `session HTTP ${session.status} — refresh Hub JWT (~15 min TTL)`); |
| 374 | process.exit(1); |
| 375 | } |
| 376 | record('auth', true, `session OK role=${session.json?.role ?? 'unknown'}`); |
| 377 | |
| 378 | const gateProbe = await apiWithDelegation('GET', '/api/v1/agent-protocol/tasks?status=pending', undefined, 'dgrnt_bearer_invalid00000000'); |
| 379 | const protocolOn = gateProbe.status !== 501 || gateProbe.json?.code !== 'external_protocol_not_authorized'; |
| 380 | record( |
| 381 | 'C1 env protocol on', |
| 382 | protocolOn && scoolingAuditAppend, |
| 383 | protocolOn && scoolingAuditAppend |
| 384 | ? `protocol reachable HTTP ${gateProbe.status}; SCOOLING_EXTERNAL_PROTOCOL_AUDIT_APPEND=enabled` |
| 385 | : !protocolOn |
| 386 | ? 'HTTP 501 external_protocol_not_authorized — flip bridge EXTERNAL_PROTOCOL_AUTHORIZED' |
| 387 | : 'set SCOOLING_EXTERNAL_PROTOCOL_AUDIT_APPEND=enabled in scooling/.env.local', |
| 388 | ); |
| 389 | if (!protocolOn || !scoolingAuditAppend) { |
| 390 | summarize(); |
| 391 | process.exit(1); |
| 392 | } |
| 393 | |
| 394 | const identityStep = await ensureExternalProviderIdentity(); |
| 395 | record('setup identity', identityStep.ok, identityStep.detail); |
| 396 | if (!identityStep.ok) { |
| 397 | summarize(); |
| 398 | process.exit(1); |
| 399 | } |
| 400 | |
| 401 | const discovered = await discoverPendingSmokeTask(); |
| 402 | let activeTaskId = discovered.taskId; |
| 403 | if (!discovered.ok || !activeTaskId) { |
| 404 | const fresh = await prepareClaimableTask('task_7d_smoke_l2', 'L2 fresh pending task', 'observe_only'); |
| 405 | record('setup fallback task_7d_smoke_l2', fresh.ok, fresh.detail); |
| 406 | if (!fresh.ok) { |
| 407 | summarize(); |
| 408 | process.exit(1); |
| 409 | } |
| 410 | activeTaskId = 'task_7d_smoke_l2'; |
| 411 | } else { |
| 412 | record('setup discover pending task', true, discovered.detail); |
| 413 | } |
| 414 | const taskMeta = await api('GET', `/api/v1/tasks/${encodeURIComponent(activeTaskId)}`); |
| 415 | const activeBoundary = taskMeta.json?.task?.boundary_policy ?? 'observe_only'; |
| 416 | record('setup task meta', true, `${activeTaskId} status=${taskMeta.json?.task?.status ?? '?'} boundary=${activeBoundary}`); |
| 417 | |
| 418 | const consentStep = await ensureConsent([activeTaskId]); |
| 419 | record('setup consent', consentStep.ok, consentStep.detail); |
| 420 | if (!consentStep.ok || !consentStep.consentId) { |
| 421 | summarize(); |
| 422 | process.exit(1); |
| 423 | } |
| 424 | |
| 425 | // C2 — claim → complete with evidence_refs |
| 426 | const grantC2 = await mintGrant(consentStep.consentId, activeTaskId); |
| 427 | record('C2 setup grant', grantC2.ok, grantC2.detail); |
| 428 | if (grantC2.ok && grantC2.bearer && grantC2.grantId) { |
| 429 | const boundary = activeBoundary; |
| 430 | const claimC2 = await claimTask(grantC2.bearer, activeTaskId, 'smoke_claim_7dl2_c2', 900); |
| 431 | const passIdC2 = claimC2.json?.pass_id; |
| 432 | const countAfterClaim = await grantActionCount(grantC2.bearer, grantC2.grantId); |
| 433 | const evidenceRefs = |
| 434 | boundary === 'propose_only' |
| 435 | ? [`pass:${passIdC2}`, 'proposal:smoke_7dl2_scope'] |
| 436 | : [`pass:${passIdC2}`]; |
| 437 | const completeOutcome = boundary === 'propose_only' ? 'completed' : 'stopped_at_boundary'; |
| 438 | const completeC2 = passIdC2 |
| 439 | ? await completeTask( |
| 440 | grantC2.bearer, |
| 441 | activeTaskId, |
| 442 | passIdC2, |
| 443 | 'smoke_complete_7dl2_c2', |
| 444 | completeOutcome, |
| 445 | evidenceRefs.filter((ref) => ref !== 'pass:null' && !ref.endsWith('null')), |
| 446 | ) |
| 447 | : { status: 0, json: {} }; |
| 448 | const countAfterComplete = await grantActionCount(grantC2.bearer, grantC2.grantId); |
| 449 | const taskAfterC2 = await api('GET', `/api/v1/tasks/${encodeURIComponent(activeTaskId)}`); |
| 450 | const c2Ok = |
| 451 | claimC2.status === 200 && |
| 452 | completeC2.status === 200 && |
| 453 | (completeOutcome !== 'completed' || |
| 454 | completeC2.json?.status === 'done' || |
| 455 | taskAfterC2.json?.task?.status === 'done') && |
| 456 | countAfterClaim !== null && |
| 457 | countAfterComplete !== null && |
| 458 | countAfterComplete > countAfterClaim; |
| 459 | record( |
| 460 | 'C2 claim → complete + audit', |
| 461 | c2Ok, |
| 462 | c2Ok |
| 463 | ? `${completeOutcome} action_count ${countAfterClaim}→${countAfterComplete} boundary=${boundary}` |
| 464 | : `claim=${claimC2.status} code=${claimC2.json?.code ?? claimC2.json?.error ?? 'none'} complete=${completeC2.status} code=${completeC2.json?.code ?? 'none'} boundary=${boundary}`, |
| 465 | ); |
| 466 | |
| 467 | // C3 — pass_id in evidence_refs round-trip (same pass as C2) |
| 468 | const c3Ok = c2Ok && passIdC2 && completeC2.status === 200; |
| 469 | record( |
| 470 | 'C3 pass_id evidence round-trip', |
| 471 | c3Ok, |
| 472 | c3Ok ? `evidence_refs included pass:${passIdC2.slice(0, 12)}…` : 'depends on C2 complete success', |
| 473 | ); |
| 474 | } else { |
| 475 | record('C2 claim → complete + audit', false, 'grant mint failed'); |
| 476 | record('C3 pass_id evidence round-trip', false, 'depends on C2'); |
| 477 | } |
| 478 | |
| 479 | // C4 — stopped_at_boundary |
| 480 | const grantC4 = await mintGrant(consentStep.consentId, activeTaskId); |
| 481 | if (grantC4.ok && grantC4.bearer && grantC4.grantId) { |
| 482 | const claimC4 = await claimTask(grantC4.bearer, activeTaskId, 'smoke_claim_7dl2_c4', 900); |
| 483 | const passIdC4 = claimC4.json?.pass_id; |
| 484 | const countBefore = await grantActionCount(grantC4.bearer, grantC4.grantId); |
| 485 | const completeC4 = passIdC4 |
| 486 | ? await completeTask(grantC4.bearer, activeTaskId, passIdC4, 'smoke_complete_7dl2_c4', 'stopped_at_boundary', [ |
| 487 | `pass:${passIdC4}`, |
| 488 | ]) |
| 489 | : { status: 0, json: {} }; |
| 490 | const countAfter = await grantActionCount(grantC4.bearer, grantC4.grantId); |
| 491 | const c4Ok = |
| 492 | claimC4.status === 200 && |
| 493 | completeC4.status === 200 && |
| 494 | countBefore !== null && |
| 495 | countAfter !== null && |
| 496 | countAfter > countBefore; |
| 497 | record( |
| 498 | 'C4 stopped_at_boundary audit', |
| 499 | c4Ok, |
| 500 | c4Ok |
| 501 | ? `200 external_boundary_stop path action_count ${countBefore}→${countAfter}` |
| 502 | : `claim=${claimC4.status} complete=${completeC4.status} code=${completeC4.json?.code ?? 'none'}`, |
| 503 | ); |
| 504 | } else { |
| 505 | record('C4 stopped_at_boundary audit', false, 'grant mint failed'); |
| 506 | } |
| 507 | |
| 508 | // C5 — boundary violation on observe_only + completed |
| 509 | const grantC5 = await mintGrant(consentStep.consentId, activeTaskId); |
| 510 | if (grantC5.ok && grantC5.bearer) { |
| 511 | const claimC5 = await claimTask(grantC5.bearer, activeTaskId, 'smoke_claim_7dl2_c5', 900); |
| 512 | const passIdC5 = claimC5.json?.pass_id; |
| 513 | const completeC5 = passIdC5 |
| 514 | ? await completeTask(grantC5.bearer, activeTaskId, passIdC5, 'smoke_complete_7dl2_c5', 'completed', []) |
| 515 | : { status: 0, json: {} }; |
| 516 | const c5Ok = completeC5.status === 422 && completeC5.json?.code === 'boundary_violation_prevented'; |
| 517 | record( |
| 518 | 'C5 boundary violation prevented', |
| 519 | c5Ok, |
| 520 | c5Ok ? '422 boundary_violation_prevented' : `HTTP ${completeC5.status} code=${completeC5.json?.code ?? 'none'}`, |
| 521 | ); |
| 522 | } else { |
| 523 | record('C5 boundary violation prevented', false, 'grant mint failed'); |
| 524 | } |
| 525 | |
| 526 | // C6 — lease expiry mid-pass |
| 527 | const grantC6 = await mintGrant(consentStep.consentId, activeTaskId); |
| 528 | if (grantC6.ok && grantC6.bearer) { |
| 529 | const claimC6 = await claimTask(grantC6.bearer, activeTaskId, 'smoke_claim_7dl2_c6', 2); |
| 530 | const passIdC6 = claimC6.json?.pass_id; |
| 531 | if (passIdC6) { |
| 532 | await sleep(2500); |
| 533 | const completeC6 = await completeTask( |
| 534 | grantC6.bearer, |
| 535 | activeTaskId, |
| 536 | passIdC6, |
| 537 | 'smoke_complete_7dl2_c6', |
| 538 | 'completed', |
| 539 | [], |
| 540 | ); |
| 541 | const sweep = await apiWithDelegation( |
| 542 | 'GET', |
| 543 | `/api/v1/agent-protocol/tasks?status=pending&actor_agent_id=${encodeURIComponent(agentId)}`, |
| 544 | undefined, |
| 545 | grantC6.bearer, |
| 546 | ); |
| 547 | const taskAfter = await api('GET', `/api/v1/tasks/${encodeURIComponent(activeTaskId)}`); |
| 548 | const c6Ok = |
| 549 | completeC6.status === 410 && |
| 550 | completeC6.json?.code === 'lease_expired' && |
| 551 | (taskAfter.json?.task?.status === 'pending' || sweep.json?.tasks?.some((t) => t.task_id === activeTaskId)); |
| 552 | record( |
| 553 | 'C6 lease expired mid-pass', |
| 554 | c6Ok, |
| 555 | c6Ok |
| 556 | ? `410 lease_expired task→pending` |
| 557 | : `complete HTTP ${completeC6.status} code=${completeC6.json?.code ?? 'none'} task=${taskAfter.json?.task?.status ?? 'unknown'}`, |
| 558 | ); |
| 559 | } else { |
| 560 | record('C6 lease expired mid-pass', false, `claim HTTP ${claimC6.status}`); |
| 561 | } |
| 562 | } else { |
| 563 | record('C6 lease expired mid-pass', false, 'grant mint failed'); |
| 564 | } |
| 565 | |
| 566 | summarize(); |
| 567 | process.exit(results.some((r) => !r.ok) ? 1 : 0); |
| 568 | } |
| 569 | |
| 570 | function summarize() { |
| 571 | const failed = results.filter((r) => !r.ok); |
| 572 | console.log(`\nSummary: ${results.length - failed.length}/${results.length} PASS`); |
| 573 | if (failed.length) { |
| 574 | console.log('Failed:', failed.map((f) => f.step).join(', ')); |
| 575 | } else { |
| 576 | console.log('7D-L2 complete+audit smoke PASS (§16.2 C1–C6)'); |
| 577 | } |
| 578 | } |
| 579 | |
| 580 | main().catch((err) => { |
| 581 | console.error(err); |
| 582 | process.exit(1); |
| 583 | }); |
File History
1 commit
sha256:baa800aedf841dbf32081aa7b2befa288ac33dfc7175ac55014c55c4d8c742f9
docs: move durable-auth freeze/evidence to local developmen…
Human
11 days ago