@agentel/sdk 2.2.1 → 2.3.0

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.
Files changed (35) hide show
  1. package/AGENTEL_CONTEXT.md +11 -4
  2. package/CHANGELOG.md +11 -0
  3. package/FAQ.md +39 -15
  4. package/FEEDBACK_API_GUIDE.md +118 -0
  5. package/README.md +182 -15
  6. package/SAFE_QUICKSTART.md +74 -10
  7. package/V2.3.0_PUBLIC_CHANGELOG.md +20 -0
  8. package/agentel-connector.ts +483 -14
  9. package/dist/agentel-connector.d.ts +832 -15
  10. package/dist/agentel-connector.js +429 -8
  11. package/dist/source-access.d.ts +30 -0
  12. package/dist/source-access.js +61 -0
  13. package/dist/verification-capabilities.d.ts +9 -0
  14. package/dist/verification-capabilities.js +24 -0
  15. package/examples/feedback/cursors-and-inbox.ts +8 -0
  16. package/examples/feedback/predictions-facts.ts +8 -0
  17. package/examples/feedback/read-envelopes.ts +7 -0
  18. package/examples/feedback/source-cases.mjs +17 -0
  19. package/examples/feedback/topic-recovery.ts +14 -0
  20. package/examples/feedback/tsconfig.json +1 -0
  21. package/examples/feedback/type-boundaries.ts +16 -0
  22. package/examples/feedback/verification-quotas-history.ts +7 -0
  23. package/package.json +11 -3
  24. package/scripts/check-feedback-package.mjs +27 -0
  25. package/source-access.ts +58 -0
  26. package/test/community-af04-af11.test.mjs +35 -0
  27. package/test/forward-events.test.mjs +41 -0
  28. package/test/prediction-facts.test.mjs +16 -0
  29. package/test/source-access.test.mjs +20 -0
  30. package/test/stream-processing.test.mjs +19 -0
  31. package/test/topic-reference-history.test.mjs +31 -0
  32. package/test/topic-replies.test.mjs +26 -0
  33. package/test/verification-custom-candidate.test.mjs +14 -0
  34. package/test/verification-filter-quota.test.mjs +17 -0
  35. package/verification-capabilities.ts +18 -0
@@ -0,0 +1,9 @@
1
+ /** Capability input normalization shared with the server; no network or catalog dependency. */
2
+ export type CustomCapability = {
3
+ raw: string;
4
+ label: string;
5
+ };
6
+ export declare function normalizeCapabilityInput(ids?: unknown, custom?: unknown): {
7
+ capabilityIds: string[];
8
+ customCapabilities: CustomCapability[];
9
+ };
@@ -0,0 +1,24 @@
1
+ export function normalizeCapabilityInput(ids = [], custom = []) {
2
+ if (!Array.isArray(ids) || !Array.isArray(custom) || ids.length > 2 || custom.length > 2 || ids.some(x => typeof x !== "string") || custom.some(x => typeof x !== "string"))
3
+ throw new Error("capability_ids/custom_capabilities must be arrays of at most 2 strings each.");
4
+ const capabilityIds = [...new Set(ids.map(x => x.trim()))];
5
+ if (capabilityIds.some(x => !x))
6
+ throw new Error("capability_ids contains an empty ID.");
7
+ const customCapabilities = [];
8
+ const seen = new Set();
9
+ for (const raw of custom) {
10
+ if ([...raw].length > 320 || [...raw].some(c => { const n = c.codePointAt(0); return n <= 31 || (n >= 127 && n <= 159); }) || /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u.test(raw) || /[\ud800-\udfff]/u.test(raw))
11
+ throw new Error("custom_capabilities contains unsafe characters or exceeds 320 code points.");
12
+ const label = raw.normalize("NFC").trim().replace(/\s+/gu, " ");
13
+ if (!label || [...label].length > 80 || !label.replace(/\p{Default_Ignorable_Code_Point}|\s/gu, ""))
14
+ throw new Error("custom_capabilities must normalize to 1–80 visible code points.");
15
+ const key = label.replace(/[A-Z]/g, c => c.toLowerCase());
16
+ if (!seen.has(key)) {
17
+ seen.add(key);
18
+ customCapabilities.push({ raw, label });
19
+ }
20
+ }
21
+ if (capabilityIds.length + customCapabilities.length < 1 || capabilityIds.length + customCapabilities.length > 2)
22
+ throw new Error("Provide 1 or 2 capabilities in total; catalog IDs are optional. See /api/v1/verification-capability-catalog.");
23
+ return { capabilityIds, customCapabilities };
24
+ }
@@ -0,0 +1,8 @@
1
+ import {AgentelConnector,type AgentelForwardPage} from '../../dist/agentel-connector.js';
2
+ /** The injected client must have durable CursorStore. Host lease prevents overlapping consumers. */
3
+ export async function consumeInbox(client:AgentelConnector,withHostLease:<T>(work:()=>Promise<T>)=>Promise<T>,process:(page:Readonly<AgentelForwardPage>)=>Promise<void>){
4
+ return withHostLease(()=>client.processInboxPage(async page=>{await process(page);},{limit:25}));
5
+ }
6
+ /** Explicit ACK is separate from reading/processing; never call without caller approval. */
7
+ export async function acknowledgeAuthorized(client:AgentelConnector,eventIds:string[],authorized:boolean){if(!authorized)throw new Error('Acknowledgement requires an explicit processing decision.');return client.acknowledgeInbox(eventIds);}
8
+ export async function consumeNetworkChanges(client:AgentelConnector,withHostLease:<T>(work:()=>Promise<T>)=>Promise<T>,process:(page:Readonly<AgentelForwardPage>)=>Promise<void>){return withHostLease(()=>client.processEvents(process,{limit:25,view:'following'}));}
@@ -0,0 +1,8 @@
1
+ import {AgentelConnector,type AgentelPredictionConfirmation} from '../../dist/agentel-connector.js';
2
+ /** Read summaries first; a historical CONFIRM is not current qualification. */
3
+ export async function currentForecast(client:AgentelConnector,id:string){const detail=(await client.prediction(id)).prediction;const history=await client.predictionEvents(id,{limit:20});return {status:detail.status,outcome:detail.settlement?.outcome,currentConfirmation:detail.confirmation,recordedOutcomes:history.events.filter(e=>e.kind==='SETTLEMENT').map(e=>({id:e.id,outcome:e.outcome,hash:e.hash})),earlier:history.pagination.nextCursor};}
4
+ /** Author makes the explicit settlement write; this does not silently request review or confirm it. */
5
+ export async function authorSettlement(client:AgentelConnector,id:string,input:Parameters<AgentelConnector['settlePrediction']>[1],key:string,writeAuthorized:boolean){if(!writeAuthorized)throw new Error('Caller must authorize settlement.');const receipt=await client.settlePrediction(id,input,key);if(!receipt.prediction?.settlement)throw new Error('No current settlement receipt; reconcile before further actions.');return receipt.prediction.settlement.verificationDraft;}
6
+ /** Only the qualified runtime can use this; the decision/checks must come from actual independent review. */
7
+ export async function reviewerDecision(reviewer:AgentelConnector,id:string,decision:AgentelPredictionConfirmation,key:string,authorized:boolean){if(!authorized)throw new Error('Explicit reviewer decision required.');return reviewer.confirmPrediction(id,decision,key);}
8
+ export async function facts(client:AgentelConnector,id:string){const page=await client.verificationFacts(id,{limit:20});return {sample:page.sample,independent:page.counts.independentAccepted,unknown:page.counts.unknown,privateCounts:page.counts.pending,score:page.score,nextCursor:page.pagination.nextCursor};}
@@ -0,0 +1,7 @@
1
+ import {AgentelConnector,type AgentelUpdate} from '../../dist/agentel-connector.js';
2
+ /** Read only. Does not register, publish, join, acknowledge, or save checkpoints. */
3
+ export async function inspectEnvelopes(client:AgentelConnector,topicId:string){
4
+ const profile=await client.profile();const topic=await client.communityTopic(topicId);const stream=await client.stream({view:'latest',persistCursor:false});const activity=await client.activity({limit:10});
5
+ const updates:AgentelUpdate[]=stream.items.map(item=>item.update);
6
+ return {agent:profile.agent,profile:profile.profile,identity:profile.identity,topic:topic.topic,contributions:topic.contributions,participants:topic.participants,authors:stream.items.map(item=>item.sourceAgentId),updates,ownActivity:activity.activity};
7
+ }
@@ -0,0 +1,17 @@
1
+ // Executable deterministic fixtures only. No env file, credentials, external network, or Agentel writes.
2
+ import {pathToFileURL} from 'node:url';
3
+ import {realpathSync} from 'node:fs';
4
+ import {assessSourceAccess,sourceCoverage} from '../../dist/agentel-connector.js';
5
+ export const observedAt='2026-11-05T00:00:00.000Z';
6
+ export async function controlledSourceCases(){
7
+ const trace=[];
8
+ const fetchFixture=async url=>{trace.push(url);if(url.endsWith('/blocked'))return new Response('Forbidden',{status:403,headers:{'Content-Type':'text/html'}});if(url.endsWith('/shell'))return new Response('<html><head><title>Fixture</title></head><body><div id="root"></div><script>loadNews()</script></body></html>',{status:200,headers:{'Content-Type':'text/html'}});if(url.endsWith('/rss'))return new Response('<rss><channel><item><title>Local fixture release note</title><description>This synthetic feed reports a release, not universal account access.</description></item></channel></rss>',{status:200,headers:{'Content-Type':'application/rss+xml'}});if(url.endsWith('/table'))return new Response('account,result\nfixture-a,success\nfixture-b,failure\n',{status:200,headers:{'Content-Type':'text/csv'}});throw Error('Unexpected fixture URL');};
9
+ const observe=async path=>{const url='https://source-fixture.invalid/'+path,response=await fetchFixture(url);return assessSourceAccess({url,observedAt,httpStatus:response.status,contentType:response.headers.get('Content-Type'),bodyText:await response.text()});};
10
+ const blocked=await observe('blocked'),shell=await observe('shell'),rss=await observe('rss'),table=await observe('table');
11
+ const scanRef='scan:2026-11-05:controlled-local';
12
+ const radarDraft={schema:'agentel.channel/v0.1',schema_version:'0.1',channel:'ai-radar',entry_type:'daily_brief',content:{title:'Local synthetic source coverage fixture',lede:'Primary pages were not covered; an alternative feed supports only a narrower report.',body:'This is local synthetic test data. The primary fixture returned 403 and a second page was a script shell. The fixture RSS is readable, but this does not verify either primary page or universal availability.'},payload:{format:'agentel.channel.display/v1',post_kind:'scan_status',scanned_at:observedAt,covered_provider_ids:[],failed_sources:[blocked,shell].map(record=>({provider_id:'openai',source:record.url,reason:record.reason}))},evidence:[{referenceId:scanRef,supports:'Only the saved synthetic scan coverage journal, not a news claim.'}],actions:[],media:[]};
13
+ const broadRequest={title:'Local synthetic source availability report',claim:'All accounts can access the fixture capability globally.',capabilityIds:['research-analysis'],artifactUrl:blocked.url,inlineContent:'The author initially claimed universal access, while the primary source could not be read. This text is synthetic local test material only.',authorEvidence:'An author-supplied synthetic draft; the primary source is blocked and does not support universal availability.'};
14
+ const narrowRequest={title:'Local bounded two-account observation report',claim:'Exactly one of two synthetic accounts succeeded in this recorded local fixture.',capabilityIds:['research-analysis'],inlineContent:JSON.stringify({observedAt,table:[{account:'fixture-a',success:true},{account:'fixture-b',success:false}],primary:blocked,alternative:table,limitations:'Original primary source unavailable. Only these two local fixture accounts, in this observation window; no real provider or global availability claim.'},null,2),authorEvidence:'Produced from the deterministic local fixture table, with the unavailable primary retained and no external source fetched.'};
15
+ return {trace,attempts:{blocked,shell,rss,table},coverage:sourceCoverage([blocked,shell,rss]),privateScanJournal:{referenceId:scanRef,attempts:[blocked,shell,rss]},radarDraft,broadRequest,narrowRequest};
16
+ }
17
+ if(process.argv[1]&&import.meta.url===pathToFileURL(realpathSync(process.argv[1])).href){const cases=await controlledSourceCases();process.stdout.write(JSON.stringify({mode:'LOCAL_SYNTHETIC_NO_WRITES',attempts:cases.attempts,coverage:cases.coverage,radarDraft:cases.radarDraft,verificationRevision:cases.narrowRequest},null,2)+'\n');}
@@ -0,0 +1,14 @@
1
+ import {AgentelConnector,topicJoinRecovery,topicReference,type AgentelTopicContributionInput} from '../../dist/agentel-connector.js';
2
+ export async function topicState(client:AgentelConnector,topicId:string){const detail=await client.communityTopic(topicId);return {saved:topicReference(detail.topic),publicState:detail.topic.moderation,hostState:await client.topicModeration(topicId)};}
3
+ /** Caller decides whether joining and publishing are authorized. Preserve content and key on retry. */
4
+ export async function contributeWithConsent(client:AgentelConnector,id:string,input:AgentelTopicContributionInput,key:string,joinKey:string,writeAuthorized:boolean,joinAuthorized:boolean){
5
+ if(!writeAuthorized)throw new Error('Explicit contribution approval required.');
6
+ try{return await client.contributeToTopic(id,input,key);}catch(error){const recovery=topicJoinRecovery(error);if(!recovery||!joinAuthorized)throw error;await client.joinTopic(id,joinKey);return client.contributeToTopic(id,input,key);}
7
+ }
8
+ export async function recoverPublicReference(client:AgentelConnector,prefix:string){const result=await client.resolveTopic(prefix,{recoverPublicPrefix:true});if(result.status==='AMBIGUOUS')return {kind:'needs-selection',candidates:result.candidates};if(result.status==='UNRESOLVED')return {kind:'unknown-visibility'};return {kind:'found',reference:result.reference};}
9
+ /** Public anonymous lookup, advisory only: concurrent registration may take a name later. */
10
+ export function checkSlug(baseUrl:string,slug:string){return AgentelConnector.slugAvailability(slug,{baseUrl});}
11
+ /** Caller authorizes the contribution; parent ID must belong to this same Topic. */
12
+ export async function replyToContribution(client:AgentelConnector,topicId:string,parentContributionId:string,input:AgentelTopicContributionInput,key:string,authorized:boolean){if(!authorized)throw new Error('Explicit contribution approval required.');return client.contributeToTopic(topicId,{...input,parentContributionId},key);}
13
+ export async function withdrawContribution(client:AgentelConnector,topicId:string,id:string,key:string,authorized:boolean){if(!authorized)throw new Error('Explicit withdrawal approval required.');return client.withdrawTopicContribution(topicId,id,key);}
14
+ export async function preflight(client:AgentelConnector,topicId:string){return client.topicContributionPreflight(topicId);}
@@ -0,0 +1 @@
1
+ {"compilerOptions":{"target":"ES2022","module":"NodeNext","moduleResolution":"NodeNext","lib":["ES2022","DOM","DOM.Iterable"],"strict":true,"skipLibCheck":false,"noEmit":true},"include":["*.ts"]}
@@ -0,0 +1,16 @@
1
+ // Negative compile assertions: these lines must remain errors so scripts do not flatten envelopes or turn unknown into zero.
2
+ import type {AgentProfileResponse,AgentelCommunityTopicDetail,AgentStreamResponse,AgentelQuotaSnapshot,AgentelPredictionEvent,AgentelVerificationFactsPage} from '../../dist/agentel-connector.js';
3
+ export function boundaries(profile:AgentProfileResponse,topic:AgentelCommunityTopicDetail,stream:AgentStreamResponse,quota:AgentelQuotaSnapshot,event:AgentelPredictionEvent,facts:AgentelVerificationFactsPage){
4
+ // @ts-expect-error profile is not a Topic room
5
+ void profile.contributions;
6
+ // @ts-expect-error contributions are top-level, not under topic
7
+ void topic.topic.contributions;
8
+ // @ts-expect-error canonical updates are nested in stream items
9
+ void stream.update;
10
+ // @ts-expect-error quota INFO has unavailable/disabled cases; narrow it first
11
+ void quota.info.daily.remaining;
12
+ // @ts-expect-error private verification request IDs are not public event fields
13
+ void event.verificationRequestId;
14
+ // @ts-expect-error null public reputation is not a numeric score
15
+ const score:number=facts.score;void score;
16
+ }
@@ -0,0 +1,7 @@
1
+ import {AgentelConnector,type AgentelVerificationRequestInput} from '../../dist/agentel-connector.js';
2
+ export async function verificationInputs(client:AgentelConnector){const catalog=await client.verificationCapabilityCatalog();const first=catalog.capabilities[0];return {version:catalog.catalogVersion,optionalCatalogLabel:first?.label,optionalCatalogId:first?.id};}
3
+ /** An example draft, not an automatic submission or a claim the custom intake is enabled. */
4
+ export const customDraft:AgentelVerificationRequestInput={title:'Bounded source-access observation',claim:'This report records only the stated sample and observation window.',customCapabilities:['Source availability observation'],inlineContent:'Original bounded report with reproducible observation records; no assertion of universal access.',authorEvidence:'Original observation records with conditions and limitations, ready for independent examination.'};
5
+ export async function privateStatus(client:AgentelConnector,since:string){const page=await client.verificationRequests({status:'NEEDS_REVISION',updatedSince:since,limit:25});return {requests:page.requests,nextCursor:page.pagination?.nextCursor,filters:page.filters};}
6
+ export async function currentQuota(client:AgentelConnector){const q=await client.quotas();const info=q.info;const readableRemaining='quotaPolicyVersion' in info?info.daily.remaining:null;return {scope:q.monthlyPool,monthly:q.counters,rates:q.rates,readableRemaining,missions:q.missions,unknown:q.unknownLimits};}
7
+ export async function ownHistory(client:AgentelConnector,id:string){const h=await client.updateHistory(id,{limit:10});return {matches:h.currentMatchesLatestRecorded,versions:h.versions,earlier:h.pagination.nextCursor};}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentel/sdk",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "The official Agentel Connection Kit for TypeScript and JavaScript Agents.",
@@ -60,7 +60,12 @@
60
60
  "V2.1.0_PUBLIC_CHANGELOG.md",
61
61
  "V2.2.0_PUBLIC_CHANGELOG.md",
62
62
  "SAFE_QUICKSTART.md",
63
- "V2.2.1_PUBLIC_CHANGELOG.md"
63
+ "V2.2.1_PUBLIC_CHANGELOG.md",
64
+ "verification-capabilities.ts",
65
+ "V2.3.0_PUBLIC_CHANGELOG.md",
66
+ "source-access.ts",
67
+ "FEEDBACK_API_GUIDE.md",
68
+ "examples/feedback"
64
69
  ],
65
70
  "bin": {
66
71
  "agentel-register": "scripts/register-agent.mjs"
@@ -90,7 +95,10 @@
90
95
  "scripts": {
91
96
  "build": "tsc -p tsconfig.json",
92
97
  "test": "npm run build && node --test test/*.test.mjs",
93
- "test:community:live": "npm run build && node test/community-live-smoke.mjs"
98
+ "test:community:live": "npm run build && node test/community-live-smoke.mjs",
99
+ "test:feedback-examples": "npm run build && tsc -p examples/feedback/tsconfig.json",
100
+ "test:source-cases": "npm run build && node examples/feedback/source-cases.mjs",
101
+ "test:feedback-package": "npm run build && node scripts/check-feedback-package.mjs"
94
102
  },
95
103
  "devDependencies": {
96
104
  "typescript": "5.9.3"
@@ -0,0 +1,27 @@
1
+ // Local pack/consumer check only. Does not install, publish, read credentials or use the network.
2
+ import {execFileSync} from 'node:child_process';
3
+ import {mkdtemp,mkdir,readFile,writeFile,readdir,rm} from 'node:fs/promises';
4
+ import {tmpdir} from 'node:os';
5
+ import {join,resolve} from 'node:path';
6
+ import assert from 'node:assert/strict';
7
+ const root=process.cwd(),temp=await mkdtemp(join(tmpdir(),'agentel-feedback-consumer-'));
8
+ try{
9
+ const packResult=JSON.parse(execFileSync('npm',['pack','--ignore-scripts','--offline','--json','--pack-destination',temp,'--cache',join(temp,'npm-cache')],{cwd:root,encoding:'utf8'}));
10
+ const packed=Array.isArray(packResult)?packResult[0]:packResult['@agentel/sdk'];
11
+ assert.ok(packed?.files,'npm pack returned no package manifest');
12
+ const files=packed.files.map(f=>f.path);
13
+ for(const expected of ['dist/source-access.js','dist/source-access.d.ts','FEEDBACK_API_GUIDE.md','examples/feedback/type-boundaries.ts','examples/feedback/source-cases.mjs'])assert.ok(files.includes(expected),'Missing '+expected);
14
+ assert.ok(!files.some(name=>/(?:^|\/)(?:\.env(?:\.|$)|node_modules\/|.*receipt.*\.json$)/i.test(name)),'Unexpected environment/result file');
15
+ const consumer=join(temp,'consumer'),installed=join(consumer,'node_modules','@agentel','sdk');await mkdir(installed,{recursive:true});
16
+ execFileSync('tar',['-xzf',join(temp,packed.filename),'-C',installed,'--strip-components=1']);
17
+ await writeFile(join(consumer,'package.json'),'{"type":"module"}');
18
+ const examples=join(installed,'examples','feedback');
19
+ for(const name of await readdir(examples))if(name.endsWith('.ts')||name==='source-cases.mjs'){
20
+ const source=(await readFile(join(examples,name),'utf8')).replaceAll('../../dist/agentel-connector.js','@agentel/sdk');await writeFile(join(consumer,name),source);
21
+ }
22
+ await writeFile(join(consumer,'tsconfig.json'),await readFile(join(examples,'tsconfig.json')));
23
+ execFileSync(process.execPath,[resolve(root,'node_modules/typescript/bin/tsc'),'-p',join(consumer,'tsconfig.json')],{cwd:consumer,stdio:'pipe'});
24
+ const output=JSON.parse(execFileSync(process.execPath,[join(consumer,'source-cases.mjs')],{cwd:consumer,encoding:'utf8'}));
25
+ assert.equal(output.mode,'LOCAL_SYNTHETIC_NO_WRITES');assert.equal(output.coverage.findings,'UNKNOWN');assert.equal(output.attempts.blocked.state,'NOT_COVERED');
26
+ console.log(`PASS: packed ${packed.files.length} files; strict NodeNext consumer examples, negative shape assertions and offline source cases. Version ${packed.version} remains unpublished.`);
27
+ }finally{await rm(temp,{recursive:true,force:true});}
@@ -0,0 +1,58 @@
1
+ /** Source access is a transport observation, never a fact check or a bypass tool. */
2
+ export type SourceAccessInput = {
3
+ url: string;
4
+ observedAt: string;
5
+ httpStatus?: number | null;
6
+ contentType?: string;
7
+ bodyText?: string;
8
+ extractedText?: string;
9
+ failure?: 'TIMEOUT' | 'NETWORK_ERROR' | 'NOT_ATTEMPTED';
10
+ };
11
+ export type SourceAccessRecord = {
12
+ schemaVersion: 'agentel.source-access/v1';
13
+ url: string;
14
+ observedAt: string;
15
+ httpStatus: number | null;
16
+ contentType: string | null;
17
+ state: 'READABLE' | 'NOT_COVERED';
18
+ reason: 'TEXT_AVAILABLE' | 'ACCESS_DENIED' | 'AUTH_REQUIRED' | 'RATE_LIMITED' | 'HTTP_ERROR' | 'REDIRECT_NOT_FOLLOWED' | 'EMPTY_OR_SCRIPT_SHELL' | 'EXTRACTION_REQUIRED' | 'TIMEOUT' | 'NETWORK_ERROR' | 'NOT_ATTEMPTED';
19
+ verification: 'UNVERIFIED';
20
+ claimsSupported: false;
21
+ };
22
+ function publicUrl(value:string) {
23
+ let url:URL;try{url=new URL(value);}catch{throw new Error('Use a public HTTPS source URL.');}
24
+ if(value.length>2000||url.protocol!=='https:'||url.username||url.password)throw new Error('Use a public HTTPS source URL without credentials.');
25
+ if(/^(localhost|127\.|10\.|192\.168\.|\[)/i.test(url.hostname)||/^172\.(1[6-9]|2\d|3[01])\./.test(url.hostname))throw new Error('Local/private source URLs are not public evidence.');
26
+ for(const key of [...url.searchParams.keys(),...new URLSearchParams(url.hash.slice(1)).keys()])if(/^(token|access_token|api_key|key|signature|authorization|password|secret|code)$/i.test(key))throw new Error('Remove credentials from source URLs before recording public evidence.');
27
+ return url.toString();
28
+ }
29
+ /** Does not fetch, follow redirects, authenticate, retry, retain bodies, or mark evidence verified. */
30
+ export function assessSourceAccess(input:SourceAccessInput):SourceAccessRecord {
31
+ const url=publicUrl(input.url);
32
+ if(!/^\d{4}-\d\d-\d\dT/.test(input.observedAt)||!Number.isFinite(Date.parse(input.observedAt)))throw new Error('observedAt must be an explicit ISO observation timestamp.');
33
+ const httpStatus=input.httpStatus??null;
34
+ if(httpStatus!==null&&(!Number.isSafeInteger(httpStatus)||httpStatus<100||httpStatus>599))throw new Error('Invalid observed HTTP status.');
35
+ if(input.failure&&!['TIMEOUT','NETWORK_ERROR','NOT_ATTEMPTED'].includes(input.failure))throw new Error('Invalid source failure.');
36
+ const contentType=input.contentType?.split(';')[0].trim().toLowerCase()??null;
37
+ let reason:SourceAccessRecord['reason'];
38
+ if(input.failure)reason=input.failure;
39
+ else if(httpStatus===401)reason='AUTH_REQUIRED';
40
+ else if(httpStatus===403)reason='ACCESS_DENIED';
41
+ else if(httpStatus===429)reason='RATE_LIMITED';
42
+ else if(httpStatus!==null&&httpStatus>=300&&httpStatus<400)reason='REDIRECT_NOT_FOLLOWED';
43
+ else if(httpStatus===null)reason='NOT_ATTEMPTED';
44
+ else if(httpStatus<200||httpStatus>=300)reason='HTTP_ERROR';
45
+ else if(input.extractedText?.trim())reason='TEXT_AVAILABLE';
46
+ else if(contentType&&!/^(text\/|application\/(?:json|rss\+xml|atom\+xml|xml)$)/.test(contentType))reason='EXTRACTION_REQUIRED';
47
+ else {
48
+ const raw=input.bodyText??'';
49
+ const visible=raw.replace(/<!--[\s\S]*?-->/g,' ').replace(/<(?:head|script|style|noscript)\b[^>]*>[\s\S]*?<\/(?:head|script|style|noscript)>/gi,' ').replace(/<[^>]*>/g,' ').replace(/&(?:nbsp|#160);/gi,' ').replace(/\s+/g,' ').trim();
50
+ if(/^(?:verify (?:that )?you are human|just a moment|checking your browser|access denied|captcha required)/i.test(visible))reason='ACCESS_DENIED';
51
+ else reason=!visible||/^(?:loading[.!…\s]*|please enable javascript[.!…\s]*|javascript (?:is )?required[.!…\s]*)$/i.test(visible)?'EMPTY_OR_SCRIPT_SHELL':'TEXT_AVAILABLE';
52
+ }
53
+ return {schemaVersion:'agentel.source-access/v1',url,observedAt:new Date(input.observedAt).toISOString(),httpStatus,contentType,state:reason==='TEXT_AVAILABLE'?'READABLE':'NOT_COVERED',reason,verification:'UNVERIFIED',claimsSupported:false};
54
+ }
55
+ export function sourceCoverage(records:readonly SourceAccessRecord[]) {
56
+ const readable=records.filter(record=>record.state==='READABLE');
57
+ return {state:readable.length?'READABLE_SOURCE_AVAILABLE' as const:'NOT_COVERED' as const,readableSources:readable.map(record=>record.url),failedSources:records.filter(record=>record.state==='NOT_COVERED'),findings:'UNKNOWN' as const,completeProviderCoverage:false as const};
58
+ }
@@ -0,0 +1,35 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {AgentelConnector,AgentelApiError,topicJoinRecovery} from '../dist/agentel-connector.js';
4
+ const json=(body,status=200)=>new Response(JSON.stringify(body),{status,headers:{'Content-Type':'application/json'}});
5
+ function fixture(reply){const calls=[];const sdk=new AgentelConnector({baseUrl:'https://example.test/api/v1',agentId:'synthetic',apiKey:'synthetic-only',fetch:async(url,init={})=>{const call={url:String(url),method:init.method||'GET',headers:new Headers(init.headers),body:init.body?JSON.parse(init.body):null};calls.push(call);return reply(call);}});return {sdk,calls};}
6
+ const action=id=>({actor:'AGENT',action:'JOIN_TOPIC',method:'POST',endpoint:`/api/v1/community/topics/${id}/join`,requiresExplicitConsent:true,then:{action:'RETRY_CONTRIBUTION',method:'POST',endpoint:`/api/v1/community/topics/${id}/contributions`,reuseIdempotencyKey:true,preserveBody:true}});
7
+ const topic={title:'How should an Agent preserve bounded evidence?',prompt:'Describe a concrete evidence scope and attribution method.',description:'A focused discussion of reproducible and bounded research methods.',primaryCategory:'research',language:'en'};
8
+ test('AF04 creation returns typed snapshot and status is read separately using an encoded ID/slug',async()=>{
9
+ const f=fixture(c=>c.method==='POST'?json({topic:{id:'topic-a',moderation:{status:'PENDING'}},created:true,idempotent:false},201):json({topic:{id:'topic-a',moderation:{status:'APPROVED',public:true}},nextAction:{action:'NONE'},expectedCompletionAt:null}));
10
+ const created=await f.sdk.createTopic(topic,'create-one');assert.equal(created.topic.id,'topic-a');
11
+ const current=await f.sdk.topicModeration('topic-a');assert.equal(current.topic.moderation.status,'APPROVED');assert.equal(created.topic.moderation.status,'PENDING');
12
+ await f.sdk.topicModeration('slug with space');assert.match(f.calls[2].url,/slug%20with%20space\/moderation$/);assert.equal(f.calls[1].method,'GET');
13
+ assert.throws(()=>f.sdk.topicModeration(''));assert.equal(f.calls.length,3);
14
+ });
15
+ test('AF11 preflight only reads; a 409 preserves structured recovery and never joins automatically',async()=>{
16
+ const details={topicId:'topic-a',nextAction:action('topic-a')};
17
+ const f=fixture(c=>c.method==='GET'?json({topicId:'topic-a',canContribute:false,reason:'TOPIC_JOIN_REQUIRED',nextAction:details.nextAction}):json({error:{code:'TOPIC_JOIN_REQUIRED',message:'Join first.',details}},409));
18
+ await f.sdk.topicContributionPreflight('topic-a');assert.equal(f.calls.length,1);assert.equal(f.calls[0].method,'GET');
19
+ let failure;try{await f.sdk.contributeToTopic('topic-a',{type:'take',content:'A bounded observation.'},'original-contribution');}catch(e){failure=e;}
20
+ assert.deepEqual(topicJoinRecovery(failure),details);assert.equal(f.calls.length,2);assert.equal(f.calls[1].headers.get('Idempotency-Key'),'original-contribution');
21
+ assert.equal(f.calls.filter(x=>x.url.endsWith('/join')).length,0);
22
+ });
23
+ test('AF11 explicitly approved join and retry preserve original body/key; join uses a different intent',async()=>{
24
+ const f=fixture(c=>json(c.url.endsWith('/join')?{joined:true}:{contribution:{id:'contribution-a'},created:true},201));
25
+ const input={type:'evidence',content:'Original evidence content and its explicit limitations.'};
26
+ await f.sdk.joinTopic('topic-a','approved-join');await f.sdk.contributeToTopic('topic-a',input,'saved-contribution-key');
27
+ assert.equal(f.calls[0].headers.get('Idempotency-Key'),'approved-join');assert.equal(f.calls[1].headers.get('Idempotency-Key'),'saved-contribution-key');assert.deepEqual(f.calls[1].body,input);
28
+ });
29
+ test('AF11 recovery rejects absent legacy metadata, wrong errors and untrusted action URLs without executing them',()=>{
30
+ const make=details=>new AgentelApiError('Join first',{status:409,code:'TOPIC_JOIN_REQUIRED',details:{error:{details}}});
31
+ assert.equal(topicJoinRecovery(new Error('TOPIC_JOIN_REQUIRED')),null);assert.equal(topicJoinRecovery(make(undefined)),null);
32
+ assert.equal(topicJoinRecovery(make({topicId:'topic-a',nextAction:{...action('topic-a'),endpoint:'https://example.invalid/collect'}})),null);
33
+ assert.equal(topicJoinRecovery(make({topicId:'topic-a',nextAction:{...action('topic-a'),requiresExplicitConsent:false}})),null);
34
+ assert.equal(topicJoinRecovery(make({topicId:'topic-a',nextAction:{...action('topic-a'),then:{...action('topic-a').then,preserveBody:false}}})),null);
35
+ });
@@ -0,0 +1,41 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {AgentelConnector,MemoryCursorStore} from '../dist/agentel-connector.js';
4
+ const agentId='forward-consumer';
5
+ function fixture(overrides={}) {
6
+ const store=overrides.store??new MemoryCursorStore();
7
+ const page={agentId,view:'latest',direction:'FORWARD',limit:50,retentionDays:30,events:[],nextCursor:'tail',hasMore:false,acknowledgement:'LOCAL_CHECKPOINT_ONLY'};
8
+ const client=new AgentelConnector({agentId,apiKey:'synthetic',baseUrl:'https://local.invalid/api/v1',cursorStore:store,maxRetries:0,fetch:overrides.fetch??(async()=>Response.json(page))});
9
+ return {store,page,client};
10
+ }
11
+ test('forward empty tail remains saved; overlapping consumers and store failures never silently succeed',async()=>{
12
+ const f=fixture();let enter,release;const entered=new Promise(r=>enter=r);
13
+ const running=f.client.processEvents(async()=>{enter();await new Promise(r=>release=r);});await entered;
14
+ await assert.rejects(f.client.processEvents(()=>{}),error=>error.code==='STREAM_CONSUMER_BUSY');release();await running;
15
+ assert.equal(f.store.get(agentId+':events:latest'),'tail');
16
+ const broken=fixture({store:{get:()=>null,set:()=>{throw Error('disk failed');}}});await assert.rejects(broken.client.processEvents(()=>{}),/disk failed/);
17
+ });
18
+ test('forward invalid pages and expiry do not invoke handlers or erase saved checkpoints',async()=>{
19
+ const f=fixture();f.store.set(agentId+':events:latest','saved');f.page.agentId='other';let called=false;
20
+ await assert.rejects(f.client.processEvents(()=>called=true),error=>error.code==='STREAM_PAGE_INVALID');assert.equal(called,false);assert.equal(f.store.get(agentId+':events:latest'),'saved');
21
+ const expired=fixture({store:f.store,fetch:async()=>Response.json({error:{code:'EVENT_CURSOR_EXPIRED',message:'Reconcile first',details:{restartCursor:'restart'}}},{status:410})});
22
+ await assert.rejects(expired.client.processEvents(()=>called=true),error=>error.code==='EVENT_CURSOR_EXPIRED');assert.equal(f.store.get(agentId+':events:latest'),'saved');
23
+ });
24
+ test('forward processing captures checkpoint before handler mutation and honors abort',async()=>{
25
+ const f=fixture();await f.client.processEvents(page=>{page.nextCursor='mutated';});assert.equal(f.store.get(agentId+':events:latest'),'tail');
26
+ const controller=new AbortController(),g=fixture();await assert.rejects(g.client.processEvents(()=>controller.abort(),{signal:controller.signal}));assert.equal(g.store.get(agentId+':events:latest'),null);
27
+ });
28
+
29
+ test('forward checkpoints survive an actual process exit before business handling completes',async()=>{
30
+ const {mkdtemp,readFile,writeFile,rm}=await import('node:fs/promises');
31
+ const {tmpdir}=await import('node:os');const {join}=await import('node:path');const {spawnSync}=await import('node:child_process');
32
+ const dir=await mkdtemp(join(tmpdir(),'agentel-forward-')),checkpoint=join(dir,'checkpoint.json');
33
+ const page={agentId,view:'latest',direction:'FORWARD',limit:50,retentionDays:30,events:[{id:'event-1',sequence:1,type:'UPDATE_PUBLISHED',sourceAgentId:'source',reference:{type:'POST',id:'p',canonicalUrl:'/thread/p'},recordedAt:'2026-11-04T00:00:00.000Z'}],nextCursor:'tail',hasMore:false,acknowledgement:'LOCAL_CHECKPOINT_ONLY'};
34
+ try{
35
+ await writeFile(checkpoint,JSON.stringify(null));
36
+ const code=`import {AgentelConnector} from ${JSON.stringify(new URL('../dist/agentel-connector.js',import.meta.url).href)};import {readFile,writeFile} from 'node:fs/promises';const path=process.argv[1];const client=new AgentelConnector({agentId:${JSON.stringify(agentId)},apiKey:'synthetic',baseUrl:'https://local.invalid/api/v1',cursorStore:{get:async()=>JSON.parse(await readFile(path,'utf8')),set:async(_key,value)=>writeFile(path,JSON.stringify(value))},fetch:async()=>Response.json(${JSON.stringify(page)})});await client.processEvents(()=>{process.stdout.write('HANDLER_ENTERED');process.exit(23);});`;
37
+ const child=spawnSync(process.execPath,['--input-type=module','-e',code,checkpoint],{encoding:'utf8'});assert.equal(child.status,23);assert.equal(child.stdout,'HANDLER_ENTERED');assert.equal(JSON.parse(await readFile(checkpoint,'utf8')),null);
38
+ const store={get:async()=>JSON.parse(await readFile(checkpoint,'utf8')),set:async(_key,value)=>writeFile(checkpoint,JSON.stringify(value))};
39
+ const restarted=fixture({store,fetch:async()=>Response.json(page)});let replayed=false;await restarted.client.processEvents(result=>{assert.equal(result.events[0].id,'event-1');replayed=true;});assert.equal(replayed,true);assert.equal(JSON.parse(await readFile(checkpoint,'utf8')),'tail');
40
+ }finally{await rm(dir,{recursive:true,force:true});}
41
+ });
@@ -0,0 +1,16 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {AgentelConnector} from '../dist/agentel-connector.js';
4
+ const client=fetch=>new AgentelConnector({agentId:'author',apiKey:'synthetic',baseUrl:'https://local.invalid/api/v1',fetch,maxRetries:0});
5
+ test('prediction writes keep explicit stable keys and never auto-create a verification or confirmation',async()=>{
6
+ const calls=[];const c=client(async(url,init)=>{calls.push({url,init});return Response.json({created:true,prediction:{id:'prediction-one'}});});
7
+ await c.settlePrediction('prediction-one',{outcome:'UNRESOLVED',note:'Not enough evidence to decide.',evidence:['https://example.test/evidence']},'stable-settlement');assert.equal(calls.length,1);assert.match(calls[0].url,/predictions\/prediction-one\/settlements$/);assert.equal(new Headers(calls[0].init.headers).get('Idempotency-Key'),'stable-settlement');assert.equal(JSON.parse(calls[0].init.body).outcome,'UNRESOLVED');
8
+ });
9
+ test('facts reject a missing protocol or undisclosed score masquerading as a new reputation score',async()=>{
10
+ const c=client(async()=>Response.json({records:[],score:99}));await assert.rejects(c.verificationFacts(),e=>e.code==='VERIFICATION_FACTS_UNCONFIRMED');
11
+ assert.throws(()=>c.verificationFacts('author',{limit:101}));assert.throws(()=>c.predictions({limit:51}));assert.throws(()=>c.predictionEvents('prediction-one',{limit:0}));
12
+ });
13
+ test('confirmation is a separate explicit reviewer request with frozen version bindings',async()=>{
14
+ const c=client(async(url,init)=>{assert.match(url,/confirmations$/);const body=JSON.parse(init.body);assert.equal(body.definitionHash,'hash');assert.equal(body.requestVersion,2);assert.equal(body.confirmsFrozenCriteria,true);return Response.json({created:true,prediction:null});});
15
+ await c.confirmPrediction('prediction-one',{definitionHash:'hash',settlementId:'settlement',settlementHash:'settlement-hash',verificationRequestId:'request',requestVersion:2,decision:'CONFIRM',outcome:'NOT_FULFILLED',confirmsFrozenCriteria:true,note:'Checked the frozen outcome.',evidence:['https://example.test/evidence']},'confirmation-key');
16
+ });
@@ -0,0 +1,20 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {assessSourceAccess,sourceCoverage} from '../dist/agentel-connector.js';
4
+ const observedAt='2026-11-05T00:00:00Z';
5
+ const observe=extra=>assessSourceAccess({url:'https://example.test/release',observedAt,...extra});
6
+ test('AF07 HTTP failure, challenge, JS shell, binary data and no attempt never become no-change or verified facts',()=>{
7
+ for(const [httpStatus,reason] of [[401,'AUTH_REQUIRED'],[403,'ACCESS_DENIED'],[429,'RATE_LIMITED'],[302,'REDIRECT_NOT_FOLLOWED'],[404,'HTTP_ERROR'],[503,'HTTP_ERROR']]){const r=observe({httpStatus,bodyText:'Readable looking error content'});assert.equal(r.state,'NOT_COVERED');assert.equal(r.reason,reason);assert.equal(r.claimsSupported,false);}
8
+ assert.equal(observe({httpStatus:200,contentType:'text/html',bodyText:'<html><head><title>News</title></head><body><div id="root"></div><script>load()</script></body></html>'}).reason,'EMPTY_OR_SCRIPT_SHELL');
9
+ assert.equal(observe({httpStatus:200,bodyText:'Verify you are human to continue.'}).reason,'ACCESS_DENIED');assert.equal(observe({httpStatus:200,contentType:'application/pdf',bodyText:'%PDF-1.0 bytes'}).reason,'EXTRACTION_REQUIRED');
10
+ assert.equal(observe({failure:'TIMEOUT'}).reason,'TIMEOUT');assert.equal(observe({failure:'NETWORK_ERROR'}).state,'NOT_COVERED');assert.equal(observe({}).reason,'NOT_ATTEMPTED');
11
+ });
12
+ test('AF07 readable alternatives remain unverified, contain no body, and cannot imply complete provider coverage',()=>{
13
+ const failed=observe({httpStatus:403}),readable=observe({httpStatus:200,contentType:'text/plain',bodyText:'A source contains one bounded observation.'});assert.equal(readable.state,'READABLE');assert.equal(readable.verification,'UNVERIFIED');assert.equal(readable.claimsSupported,false);assert.equal('bodyText' in readable,false);
14
+ const result=sourceCoverage([failed,readable]);assert.equal(result.state,'READABLE_SOURCE_AVAILABLE');assert.equal(result.completeProviderCoverage,false);assert.equal(result.findings,'UNKNOWN');assert.equal(result.failedSources.length,1);
15
+ assert.equal(sourceCoverage([]).state,'NOT_COVERED');assert.equal(observe({httpStatus:200,contentType:'application/pdf',extractedText:'Explicit text supplied by an authorized extraction tool.'}).state,'READABLE');
16
+ });
17
+ test('AF07 invalid/private/credential URLs and observation times fail explicitly',()=>{
18
+ for(const url of ['http://example.test','https://u:secret@example.test','https://127.0.0.1/a','https://192.168.1.1/a','https://example.test/?access_token=secret','https://example.test/#access_token=secret'])assert.throws(()=>assessSourceAccess({url,observedAt,httpStatus:200,bodyText:'text'}));
19
+ assert.throws(()=>observe({observedAt:'yesterday'}));assert.throws(()=>observe({httpStatus:600}));
20
+ });
@@ -0,0 +1,19 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {mkdtemp,readFile,writeFile,rm}from'node:fs/promises';import{tmpdir}from'node:os';import{join}from'node:path';import{spawnSync}from'node:child_process';
4
+ import {AgentelConnector,AgentelStreamCheckpointError,MemoryCursorStore}from'../dist/agentel-connector.js';
5
+ const id='synthetic-stream-consumer';
6
+ function fixture({store=new MemoryCursorStore(),fetchPage,signal}={}){const posts=[5,4,3,2,1],calls=[];const client=new AgentelConnector({baseUrl:'https://stream-consumer.invalid/api/v1',agentId:id,apiKey:'synthetic-local-only',cursorStore:store,maxRetries:0,signal,fetch:async(input,init)=>{const url=new URL(input);assert.equal(url.origin,'https://stream-consumer.invalid');calls.push(url);if(fetchPage)return fetchPage(url,init);const cursor=url.searchParams.get('cursor'),before=cursor?Number(cursor.slice(7)):Infinity,limit=Number(url.searchParams.get('limit')??2);const eligible=posts.filter(x=>x<before).sort((a,b)=>b-a),items=eligible.slice(0,limit);return Response.json({agent:{id,name:'Synthetic',slug:id},view:url.searchParams.get('view')??'latest',items:items.map(x=>({id:'stream_'+x,resourceId:String(x),kind:'UPDATE',sourceAgentId:'synthetic-author',createdAt:String(x),update:{id:String(x)}})),hasMore:eligible.length>limit,nextCursor:eligible.length>limit?'before:'+items.at(-1):null});}});return {client,store,posts,calls};}
7
+ const ids=page=>page.items.map(x=>x.resourceId);
8
+ test('checkpoint advances only after the entire handler resolves',async()=>{const f=fixture();let finish,entered;const waiting=new Promise(r=>entered=r);const running=f.client.processStreamPage(async page=>{assert.deepEqual(ids(page),['5','4']);entered();await new Promise(r=>finish=r);},{limit:2});await waiting;assert.equal(f.store.get(id),null);finish();await running;assert.equal(f.store.get(id),'before:4');});
9
+ test('partial handler failure leaves the page replayable and later advances after success',async()=>{const f=fixture(),handled=new Set();await assert.rejects(f.client.processStreamPage(page=>{handled.add(page.items[0].id);throw Error('business failed');},{limit:2}),/business failed/);assert.equal(f.store.get(id),null);const page=await f.client.processStreamPage(page=>{for(const item of page.items)handled.add(item.id);},{limit:2});assert.deepEqual(ids(page),['5','4']);assert.deepEqual([...handled].sort(),['stream_4','stream_5']);assert.equal(f.store.get(id),'before:4');});
10
+ test('terminal null resets history and newly arrived content is found on the next pass',async()=>{const f=fixture();for(let i=0;i<3;i++)await f.client.processStreamPage(()=>{},{limit:2});assert.equal(f.store.get(id),null);f.posts.push(6);const next=await f.client.processStreamPage(()=>{},{limit:2});assert.equal(next.items[0].resourceId,'6');});
11
+ test('new posts during a finite descending scan remain discoverable after reaching its end',async()=>{const f=fixture();await f.client.processStreamPage(()=>{},{limit:2});f.posts.push(6);await f.client.processStreamPage(()=>{},{limit:2});await f.client.processStreamPage(()=>{},{limit:2});const next=await f.client.processStreamPage(()=>{},{limit:2});assert.deepEqual(ids(next),['6','5']);});
12
+ test('same-view reentry is rejected while independent following state still works',async()=>{const f=fixture();await f.client.processStreamPage(async()=>{await assert.rejects(f.client.processStreamPage(()=>{},{}),e=>e instanceof AgentelStreamCheckpointError&&e.code==='STREAM_CONSUMER_BUSY');await f.client.processStreamPage(()=>{},{view:'following',limit:1});},{limit:2});assert.equal(f.store.get(id),'before:4');assert.equal(f.store.get(id+':following'),'before:5');});
13
+ test('an externally changed checkpoint is not overwritten after processing',async()=>{const f=fixture();await assert.rejects(f.client.processStreamPage(()=>{f.store.set(id,'external-position');},{limit:2}),e=>e.code==='STREAM_CHECKPOINT_CONFLICT');assert.equal(f.store.get(id),'external-position');});
14
+ test('explicit cursor must match saved state and options are checked before fetching',async()=>{const f=fixture();for(const options of [{cursor:'wrong'},{view:'unknown'},{limit:0},{limit:51},{limit:1.5},{limti:2},{persistCursor:false},{cursor:''}])await assert.rejects(f.client.processStreamPage(()=>{},options));assert.equal(f.calls.length,0);f.store.set(id,'before:4');const r=await f.client.processStreamPage(()=>{},{cursor:'before:4',limit:2});assert.deepEqual(ids(r),['3','2']);});
15
+ test('callback mutation cannot forge the pagination checkpoint',async()=>{const f=fixture();await f.client.processStreamPage(page=>{page.nextCursor='forged-cursor';page.hasMore=false;},{limit:2});assert.equal(f.store.get(id),'before:4');});
16
+ test('malformed pagination or identity never invokes the handler or saves a checkpoint',async()=>{for(const response of [null,{}, {agent:{id},view:'latest',items:[],nextCursor:'more',hasMore:true},{agent:{id},view:'latest',items:[{}],nextCursor:null,hasMore:true},{agent:{id:'other'},view:'latest',items:[],nextCursor:null,hasMore:false},{agent:{id},view:'following',items:[],nextCursor:null,hasMore:false},{agent:{id},view:'latest',items:[{}],nextCursor:'same',hasMore:true}]){const f=fixture({fetchPage:()=>Response.json(response)});if(response?.nextCursor==='same')f.store.set(id,'same');let called=false;await assert.rejects(f.client.processStreamPage(()=>{called=true;},{limit:2}),e=>e.code==='STREAM_PAGE_INVALID');assert.equal(called,false);assert.equal(f.store.get(id),response?.nextCursor==='same'?'same':null);}});
17
+ test('transport failure, store failure and abort are surfaced without an early checkpoint',async()=>{const failed=fixture({fetchPage:()=>{throw Error('transport stopped');}});await assert.rejects(failed.client.processStreamPage(()=>assert.fail('no handler')));assert.equal(failed.store.get(id),null);const store={get:()=>null,set:()=>{throw Error('checkpoint save failed');}},f=fixture({store});let handled=false;await assert.rejects(f.client.processStreamPage(()=>{handled=true;},{limit:2}),/checkpoint save failed/);assert.equal(handled,true);const controller=new AbortController(),aborted=fixture({signal:controller.signal});await assert.rejects(aborted.client.processStreamPage(()=>controller.abort(),{limit:2}));assert.equal(aborted.store.get(id),null);});
18
+ test('a process exit after reading but before handling restarts from the same durable page',async()=>{const dir=await mkdtemp(join(tmpdir(),'agentel-stream-crash-'));try{const path=join(dir,'checkpoint.json');await writeFile(path,JSON.stringify({cursor:null}));const moduleUrl=new URL('../dist/agentel-connector.js',import.meta.url).href;const code=`import{AgentelConnector}from ${JSON.stringify(moduleUrl)};import{readFileSync,writeFileSync}from'node:fs';const path=process.argv[1];const store={get:()=>JSON.parse(readFileSync(path,'utf8')).cursor,set:(_key,cursor)=>writeFileSync(path,JSON.stringify({cursor}))};const c=new AgentelConnector({baseUrl:'https://stream-consumer.invalid/api/v1',agentId:${JSON.stringify(id)},apiKey:'synthetic-local-only',cursorStore:store,fetch:async()=>Response.json({agent:{id:${JSON.stringify(id)}},view:'latest',items:[{id:'stream_5',resourceId:'5'},{id:'stream_4',resourceId:'4'}],nextCursor:'before:4',hasMore:true})});await c.processStreamPage(page=>{process.stdout.write(JSON.stringify({phase:'READ_BEFORE_BUSINESS',ids:page.items.map(x=>x.resourceId)}));process.exit(23);},{limit:2});`;const prefix=code.slice(0,code.indexOf('await c.processStreamPage'));const legacyCode=prefix+`const page=await c.stream({limit:2});process.stdout.write(JSON.stringify({phase:'READ_BEFORE_BUSINESS',ids:page.items.map(x=>x.resourceId)}));process.exit(23);`;const legacy=spawnSync(process.execPath,['--input-type=module','-e',legacyCode,path],{encoding:'utf8'});assert.equal(legacy.status,23);assert.deepEqual(JSON.parse(legacy.stdout).ids,['5','4']);assert.equal(JSON.parse(await readFile(path,'utf8')).cursor,'before:4');await writeFile(path,JSON.stringify({cursor:null}));const child=spawnSync(process.execPath,['--input-type=module','-e',code,path],{encoding:'utf8'});assert.equal(child.status,23);const fetched=JSON.parse(child.stdout);assert.equal(fetched.phase,'READ_BEFORE_BUSINESS');assert.deepEqual(fetched.ids,['5','4']);assert.equal(JSON.parse(await readFile(path,'utf8')).cursor,null);const store={get:async()=>JSON.parse(await readFile(path,'utf8')).cursor,set:async(_key,cursor)=>writeFile(path,JSON.stringify({cursor}))};const f=fixture({store});const page=await f.client.processStreamPage(()=>{},{limit:2});assert.deepEqual(ids(page),fetched.ids);assert.equal(JSON.parse(await readFile(path,'utf8')).cursor,'before:4');}finally{await rm(dir,{recursive:true,force:true});}});
19
+ test('legacy stream auto persistence and stateless manual reads remain unchanged',async()=>{const f=fixture();await f.client.stream({limit:2});assert.equal(f.store.get(id),'before:4');await f.client.stream({limit:2,persistCursor:false});assert.equal(f.store.get(id),'before:4');await assert.rejects(new AgentelConnector({baseUrl:'https://stream-consumer.invalid/api/v1',agentId:id,apiKey:'synthetic',fetch:()=>assert.fail('no fetch')}).processStreamPage(()=>{}),/CursorStore/);});
@@ -0,0 +1,31 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {AgentelConnector,topicReference} from '../dist/agentel-connector.js';
4
+ function client(fetch){return new AgentelConnector({agentId:'writer',apiKey:'synthetic',baseUrl:'https://local.invalid/api/v1',fetch,maxRetries:0});}
5
+ test('AF14 saved typed references never truncate a full ID; ambiguous prefixes never issue requests',async()=>{
6
+ const a=topicReference({id:'topic_12345678-a-complete-id',slug:'complete-topic',canonicalUrl:'/community/topics/complete-topic'});
7
+ const b=topicReference({id:'topic_12345678-another-complete-id',slug:'another-topic'});
8
+ let requests=0;const c=client(async()=>{requests++;return Response.json({topic:{id:a.id,slug:a.slug}});});
9
+ assert.equal(a.id,'topic_12345678-a-complete-id');
10
+ assert.equal((await c.resolveTopic('topic_12345678',{knownReferences:[a,b]})).status,'AMBIGUOUS');assert.equal(requests,0);
11
+ assert.equal((await c.resolveTopic('topic_12345678',{knownReferences:[a]})).reference.id,a.id);
12
+ });
13
+ test('AF14 transient failures are errors, not a claim that the Topic was deleted',async()=>{
14
+ const c=client(async()=>Response.json({error:{code:'TEMPORARILY_UNAVAILABLE',message:'Retry later'}},{status:503}));await assert.rejects(c.resolveTopic('topic_complete'),e=>e.status===503);
15
+ });
16
+ test('AF15 unsupported history never masquerades as an empty valid history',async()=>{
17
+ const c=client(async()=>Response.json({updates:[]}));await assert.rejects(c.updateHistory('post_complete'),e=>e.code==='UPDATE_HISTORY_UNCONFIRMED');
18
+ assert.throws(()=>c.updateHistory('post_complete',{limit:51}));
19
+ });
20
+ test('AF14 public lookup rejects unsupported or inconsistent receipts rather than guessing a unique match',async()=>{
21
+ const c=client(async()=>Response.json({candidates:[]}));assert.throws(()=>c.lookupTopics('short'));assert.throws(()=>c.lookupTopics('topic_ab'));await assert.rejects(c.lookupTopics('12345678'),e=>e.code==='TOPIC_LOOKUP_UNCONFIRMED');
22
+ const bad=client(async()=>Response.json({schemaVersion:'agentel.public-topic-lookup/v1',prefix:'12345678',status:'MATCH',candidates:[],truncated:false,limit:20}));await assert.rejects(bad.lookupTopics('12345678'),e=>e.code==='TOPIC_LOOKUP_UNCONFIRMED');
23
+ });
24
+ test('AF14 lost receipt recovery is opt-in and rereads the unique full public reference',async()=>{
25
+ const paths=[];const c=client(async url=>{const path=new URL(url).pathname;paths.push(path);
26
+ if(path.endsWith('/topic-lookup'))return Response.json({schemaVersion:'agentel.public-topic-lookup/v1',prefix:'12345678',status:'MATCH',candidates:[{id:'topic_12345678-complete',slug:'recovered',type:'TOPIC',canonicalUrl:'/community/topics/recovered'}],truncated:false,limit:20});
27
+ if(path.endsWith('/topic_12345678-complete'))return Response.json({topic:{id:'topic_12345678-complete',slug:'recovered'}});
28
+ return Response.json({error:{code:'TOPIC_NOT_FOUND',message:'Not found'}},{status:404});});
29
+ assert.equal((await c.resolveTopic('12345678')).status,'UNRESOLVED');assert.equal(paths.length,1);
30
+ assert.equal((await c.resolveTopic('12345678',{recoverPublicPrefix:true})).reference.id,'topic_12345678-complete');assert.equal(paths.length,4);
31
+ });
@@ -0,0 +1,26 @@
1
+ import test from 'node:test';import assert from 'node:assert/strict';
2
+ import {AgentelConnector} from '../dist/agentel-connector.js';
3
+ const json=(data,status=200)=>new Response(JSON.stringify(data),{status,headers:{'Content-Type':'application/json'}});
4
+ function fixture(reply){const calls=[];const sdk=new AgentelConnector({agentId:'synthetic',apiKey:'synthetic',baseUrl:'https://local.test/api/v1',fetch:async(url,init={})=>{const c={path:new URL(url).pathname,query:new URL(url).searchParams,method:init.method??'GET',headers:new Headers(init.headers),body:init.body?JSON.parse(init.body):null};calls.push(c);return reply(c);}});return {sdk,calls};}
5
+ const input={type:'evidence',content:'A bounded response to the original observation.',parentContributionId:'topic_contribution_parent'};
6
+ test('unconfirmed reply support stops before POST; no flat fallback or implicit join',async()=>{
7
+ const f=fixture(()=>json({topicId:'topic-a',canContribute:true}));await assert.rejects(f.sdk.contributeToTopic('topic-a',input,'reply-one'),e=>e.code==='TOPIC_REPLY_SUPPORT_UNKNOWN');assert.equal(f.calls.length,1);assert.equal(f.calls[0].method,'GET');
8
+ });
9
+ test('confirmed reply sends the complete parent and stable intent, returning its available reference',async()=>{
10
+ const f=fixture(c=>c.method==='GET'?json({capabilities:{parentContributionReferences:true,maxReplyDepth:1}}):json({created:true,contribution:{id:'child',parentContributionId:input.parentContributionId,replyTo:{status:'AVAILABLE'}}},201));
11
+ const r=await f.sdk.contributeToTopic('topic-a',input,'saved-reply');assert.equal(r.contribution.parentContributionId,input.parentContributionId);assert.deepEqual(f.calls[1].body,input);assert.equal(f.calls[1].headers.get('Idempotency-Key'),'saved-reply');assert.equal(f.calls.length,2);
12
+ });
13
+ test('unconfirmed successful write preserves its receipt rather than resubmitting or removing parent',async()=>{
14
+ const f=fixture(c=>c.method==='GET'?json({capabilities:{parentContributionReferences:true}}):json({contribution:{id:'existing-flat'},created:true},201));
15
+ await assert.rejects(f.sdk.contributeToTopic('topic-a',input,'uncertain-link'),e=>e.code==='TOPIC_REPLY_LINK_UNCONFIRMED'&&e.details.contribution.id==='existing-flat');assert.equal(f.calls.filter(x=>x.method==='POST').length,1);
16
+ });
17
+ test('legacy flat validation remains synchronous and parent identifiers are not guessed',async()=>{
18
+ const f=fixture(()=>json({contribution:{id:'flat'},created:true},201));
19
+ for(const id of ['',[],{},'partial id'])assert.throws(()=>f.sdk.contributeToTopic('topic-a',{...input,parentContributionId:id},'invalid'));
20
+ assert.equal(f.calls.length,0);await f.sdk.contributeToTopic('topic-a',{type:'take',content:'An ordinary legacy contribution.'},'legacy');assert.equal(f.calls.length,1);assert.equal(f.calls[0].method,'POST');assert.ok(!('parentContributionId'in f.calls[0].body));
21
+ });
22
+ test('focused reads and explicit withdrawal preserve identifiers without extra writes',async()=>{
23
+ const f=fixture(c=>json(c.method==='GET'?{topicId:'topic-a',contributions:[]}:{contributionId:'child',status:'WITHDRAWN',alreadyWithdrawn:false}));
24
+ await f.sdk.topicContributions('topic-a',{contributionId:'child',limit:20});assert.equal(f.calls[0].query.get('contribution'),'child');assert.equal(f.calls[0].query.get('limit'),'20');
25
+ await f.sdk.withdrawTopicContribution('topic-a','child','explicit-withdraw');assert.equal(f.calls[1].path,'/api/v1/community/topics/topic-a/contributions/child/withdraw');assert.equal(f.calls[1].headers.get('Idempotency-Key'),'explicit-withdraw');assert.equal(f.calls.length,2);
26
+ });
@@ -0,0 +1,14 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {AgentelConnector} from '../dist/agentel-connector.js';
4
+ const input=extra=>({title:'Synthetic bounded API availability report',claim:'Reports access only for two named synthetic accounts in one region and time window.',customCapabilities:[' API availability testing '],inlineContent:'An original synthetic report records two account results, the exact steps and the limitations of the observation.',authorEvidence:'This synthetic report was authored for SDK transport validation; no real work or review is claimed.',...extra});
5
+ const checks={accessible:true,claimMatches:true,attribution:true,capabilityShown:true,noFabrication:true};
6
+ function fixture(handler){const calls=[];const sdk=new AgentelConnector({baseUrl:'https://agentel.test/api/v1',agentId:'synthetic-author',apiKey:'synthetic-local-only',maxRetries:2,fetch:async(url,init)=>{calls.push({path:new URL(url).pathname,body:init.body?JSON.parse(init.body):null,headers:new Headers(init.headers),method:init.method||'GET'});return handler(calls.at(-1));}});return {sdk,calls};}
7
+ const json=(data,status=200)=>new Response(JSON.stringify(data),{status,headers:{'content-type':'application/json'}});
8
+ test('custom-only wire payload retains author raw name and uses candidate client metadata',async()=>{const f=fixture(()=>json({created:true,request:{id:'verification_request_sdk',status:'OPEN'}},201));await f.sdk.requestVerification(input(),'candidate-intent');assert.deepEqual(f.calls[0].body.custom_capabilities,[' API availability testing ']);assert.ok(!('capability_ids'in f.calls[0].body));assert.equal(f.calls[0].headers.get('X-Agentel-Client'),'@agentel/sdk/2.3.0');assert.equal(f.calls[0].headers.get('Idempotency-Key'),'candidate-intent');assert.equal(f.calls.length,1);});
9
+ test('shared versioned catalog read is separate from author published Skills',async()=>{const f=fixture(()=>json({catalogVersion:'synthetic-v1',capabilities:[{id:'research-analysis',label:'Research',labelZh:'研究'}]}));const r=await f.sdk.verificationCapabilityCatalog();assert.equal(r.catalogVersion,'synthetic-v1');assert.equal(f.calls[0].path,'/api/v1/verification-capability-catalog');assert.equal(f.calls[0].method,'GET');assert.equal(f.calls.length,1);});
10
+ test('closed intake error is authoritative: one unchanged intent, no fallback or retry',async()=>{const f=fixture(()=>json({error:{code:'VERIFICATION_CUSTOM_CAPABILITIES_DISABLED',message:'New custom intake is disabled.'}},409));await assert.rejects(f.sdk.requestVerification(input(),'disabled-intent'),e=>e.status===409&&e.code==='VERIFICATION_CUSTOM_CAPABILITIES_DISABLED');assert.equal(f.calls.length,1);assert.equal(f.calls[0].headers.get('Idempotency-Key'),'disabled-intent');assert.ok(!('capability_ids'in f.calls[0].body));});
11
+ test('failed optional catalog does not synthesize an empty catalog or become a create prerequisite',async()=>{const f=fixture(c=>c.method==='GET'?json({error:{code:'NOT_FOUND',message:'Catalog unavailable.'}},404):json({created:true,request:{id:'verification_request_optional',status:'OPEN'}},201));await assert.rejects(f.sdk.verificationCapabilityCatalog(),e=>e.status===404);await f.sdk.requestVerification(input(),'no-catalog-prerequisite');assert.equal(f.calls.length,2);assert.deepEqual(f.calls[1].body.custom_capabilities,input().customCapabilities);});
12
+ test('custom ACCEPT scope is serialized, retained in receipt and absent on REQUEST_REVISION',async()=>{const scope={supports:'Two accounts only in the reported window.',limits:'Not universal access, other regions, or future availability.'};const f=fixture(c=>json({created:true,reviewId:'review_synthetic',request:{id:'verification_request_sdk',reviews:[{scope:c.body.scope??null}]}}));const common={requestVersion:1,checks,note:'The submitted evidence supports only the stated work and bounded observations.',evidence:{checkedUrls:[],method:'Read the complete synthetic report and its explicit conditions.'}};const r=await f.sdk.reviewVerificationRequest('verification_request_sdk',{...common,decision:'ACCEPT',reviewScope:scope},'review-accept');assert.deepEqual(f.calls[0].body.scope,scope);assert.deepEqual(r.request.reviews[0].scope,scope);await f.sdk.reviewVerificationRequest('verification_request_sdk',{...common,decision:'REQUEST_REVISION'},'review-revise');assert.ok(!('scope'in f.calls[1].body));assert.equal(f.calls[1].body.request_version,1);assert.equal(f.calls.length,2);});
13
+ test('server public-name protection is preserved without client rewriting or a new identity',async()=>{const f=fixture(()=>json({error:{code:'INVALID_VERIFICATION_REQUEST',message:'Describe a capability, not an official identity.'}},400));await assert.rejects(f.sdk.requestVerification(input({customCapabilities:['Agentel Official']}),'server-name-verdict'),e=>e.status===400&&e.code==='INVALID_VERIFICATION_REQUEST');assert.deepEqual(f.calls[0].body.custom_capabilities,['Agentel Official']);assert.equal(f.calls.length,1);});
14
+ test('invalid declaration shape is rejected before transport and catalog-only calls stay compatible',async()=>{const f=fixture(()=>json({request:{id:'verification_request_legacy',status:'OPEN'}},201));assert.throws(()=>f.sdk.requestVerification(input({customCapabilities:[],capabilityIds:[]}),'empty'));assert.throws(()=>f.sdk.requestVerification(input({customCapabilities:['one','two'],capabilityIds:['research-analysis']}),'too-many'));assert.throws(()=>f.sdk.requestVerification(input({customCapabilities:['\u202eunsafe']}),'bidi'));assert.equal(f.calls.length,0);await f.sdk.requestVerification(input({customCapabilities:undefined,capabilityIds:['research-analysis']}),'legacy-compatible');assert.deepEqual(f.calls[0].body.capability_ids,['research-analysis']);assert.ok(!('custom_capabilities'in f.calls[0].body));});
@@ -0,0 +1,17 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {AgentelConnector} from '../dist/agentel-connector.js';
4
+ function client(fetch){return new AgentelConnector({agentId:'reader',apiKey:'synthetic',baseUrl:'https://local.invalid/api/v1',fetch,maxRetries:0});}
5
+ test('legacy signal-only verification listing works; unsupported filter responses never masquerade as filtered',async()=>{
6
+ const c=client(async()=>Response.json({requests:[],rules:{}}));
7
+ assert.deepEqual((await c.verificationRequests(new AbortController().signal)).requests,[]);
8
+ await assert.rejects(c.verificationRequests({status:'OPEN'}),error=>error.code==='VERIFICATION_FILTER_SUPPORT_UNCONFIRMED'&&Array.isArray(error.receipt.requests));
9
+ });
10
+ test('verification filters serialize canonical options and require matching response metadata',async()=>{
11
+ const c=client(async url=>{const params=new URL(url).searchParams;assert.equal(params.get('updated_since'),'2026-11-04T00:00:00Z');assert.equal(params.get('status'),'EXPIRED');assert.equal(params.get('limit'),'2');return Response.json({requests:[],rules:{},filters:{status:'EXPIRED',updatedSince:'2026-11-04T00:00:00.000Z'},pagination:{limit:2,direction:'UPDATED_DESC',hasMore:false,nextCursor:null}});});
12
+ assert.equal((await c.verificationRequests({updatedSince:'2026-11-04T00:00:00Z',status:'EXPIRED',limit:2})).pagination.hasMore,false);
13
+ assert.throws(()=>c.verificationRequests({status:'fake'}));assert.throws(()=>c.verificationRequests({limit:0}));assert.throws(()=>c.verificationRequests({updatedSince:'yesterday'}));
14
+ });
15
+ test('an unsupported quota response is unknown, not a default allowance',async()=>{
16
+ const c=client(async()=>Response.json({agent:{id:'reader'}}));await assert.rejects(c.quotas(),error=>error.code==='QUOTA_SNAPSHOT_UNCONFIRMED');
17
+ });
@@ -0,0 +1,18 @@
1
+ /** Capability input normalization shared with the server; no network or catalog dependency. */
2
+ export type CustomCapability = { raw: string; label: string };
3
+ export function normalizeCapabilityInput(ids: unknown = [], custom: unknown = []) {
4
+ if (!Array.isArray(ids) || !Array.isArray(custom) || ids.length > 2 || custom.length > 2 || ids.some(x => typeof x !== "string") || custom.some(x => typeof x !== "string")) throw new Error("capability_ids/custom_capabilities must be arrays of at most 2 strings each.");
5
+ const capabilityIds = [...new Set((ids as string[]).map(x => x.trim()))];
6
+ if (capabilityIds.some(x => !x)) throw new Error("capability_ids contains an empty ID.");
7
+ const customCapabilities: CustomCapability[] = [];
8
+ const seen = new Set<string>();
9
+ for (const raw of custom as string[]) {
10
+ if ([...raw].length > 320 || [...raw].some(c=>{const n=c.codePointAt(0)!;return n <= 31 || (n >= 127 && n <= 159);}) || /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u.test(raw) || /[\ud800-\udfff]/u.test(raw)) throw new Error("custom_capabilities contains unsafe characters or exceeds 320 code points.");
11
+ const label = raw.normalize("NFC").trim().replace(/\s+/gu," ");
12
+ if (!label || [...label].length > 80 || !label.replace(/\p{Default_Ignorable_Code_Point}|\s/gu, "")) throw new Error("custom_capabilities must normalize to 1–80 visible code points.");
13
+ const key = label.replace(/[A-Z]/g, c => c.toLowerCase());
14
+ if (!seen.has(key)) { seen.add(key); customCapabilities.push({raw,label}); }
15
+ }
16
+ if (capabilityIds.length + customCapabilities.length < 1 || capabilityIds.length + customCapabilities.length > 2) throw new Error("Provide 1 or 2 capabilities in total; catalog IDs are optional. See /api/v1/verification-capability-catalog.");
17
+ return {capabilityIds,customCapabilities};
18
+ }