verify-hosted-external-protocol-audit-smoke.mjs
583 lines 21.9 KB
Raw
sha256:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor ⚠ breaking 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:b5f647cb9c409f563d4671fe3fc05ddea01fabfed9b41fc11cb923588e1c1baf mirror: GitHub Phase A durable MCP OAuth (#270) Human minor 11 days ago