@agentunion/fastaun 0.5.18 → 0.5.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +172 -79
- package/_packed_docs/CHANGELOG.md +172 -79
- package/_packed_docs/INDEX.md +244 -116
- package/_packed_docs/KITE_DOCS_GUIDE.md +98 -69
- package/_packed_docs/agent.md/SCHEMA.md +245 -205
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
- package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +42 -15
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +4 -4
- package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +45 -134
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1072 -1072
- package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +5 -20
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +687 -668
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +19 -28
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +443 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +658 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +4 -0
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +68 -61
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +279 -285
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
- package/_packed_docs/sdk/09-group-rpc-manual.md +271 -235
- package/_packed_docs/sdk/09-message-rpc-manual.md +47 -6
- package/_packed_docs/sdk/09-storage-rpc-manual.md +70 -68
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +63 -34
- package/_packed_docs/sdk/INDEX.md +105 -34
- package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +922 -0
- package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +300 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +382 -0
- package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +476 -0
- package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
- package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
- package/dist/agent-md-schema.d.ts +15 -1
- package/dist/agent-md-schema.js +87 -24
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +54 -2
- package/dist/agent-md.js +637 -53
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +38 -59
- package/dist/aid-store.js +300 -265
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.js +30 -11
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +6 -1
- package/dist/auth.js +500 -15
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +44 -2
- package/dist/cert-utils.js +149 -49
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +11 -1
- package/dist/client/delivery.js +321 -62
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +1 -0
- package/dist/client/rpc-pipeline.js +142 -82
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +1 -0
- package/dist/client/v2-e2ee.js +166 -96
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +84 -1
- package/dist/client.js +780 -30
- package/dist/client.js.map +1 -1
- package/dist/errors.js +4 -3
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +9 -3
- package/dist/events.js +69 -11
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +83 -3
- package/dist/facades.js +100 -6
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +5 -0
- package/dist/keystore/aid-db.js +57 -0
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +8 -0
- package/dist/keystore/local-identity-store.js +10 -3
- package/dist/keystore/local-identity-store.js.map +1 -1
- package/dist/keystore/local-token-store.d.ts +6 -1
- package/dist/keystore/local-token-store.js +24 -1
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +98 -0
- package/dist/org-protocol.js +1753 -0
- package/dist/org-protocol.js.map +1 -0
- package/dist/ownership-proof.d.ts +82 -5
- package/dist/ownership-proof.js +327 -31
- package/dist/ownership-proof.js.map +1 -1
- package/dist/ownership-v21.d.ts +196 -0
- package/dist/ownership-v21.js +712 -0
- package/dist/ownership-v21.js.map +1 -0
- package/dist/ownership-workflow.d.ts +87 -0
- package/dist/ownership-workflow.js +1182 -0
- package/dist/ownership-workflow.js.map +1 -0
- package/dist/seq-tracker.d.ts +1 -0
- package/dist/seq-tracker.js +10 -1
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +90 -1
- package/dist/tools/cross-sdk-agent.js +441 -7
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +8 -0
- package/dist/transport.js +118 -4
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/decrypt.d.ts +1 -0
- package/dist/v2/e2ee/decrypt.js +9 -1
- package/dist/v2/e2ee/decrypt.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/agent-md.js
CHANGED
|
@@ -3,13 +3,58 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as http from 'node:http';
|
|
4
4
|
import * as https from 'node:https';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
7
|
+
import { parseDocument } from 'yaml';
|
|
6
8
|
import { AID } from './aid.js';
|
|
7
|
-
import { certMatchesFingerprint, parseAgentMdTailSignature } from './cert-utils.js';
|
|
8
|
-
import { validateAgentMdDocument, verifyAgentMdUpload } from './agent-md-schema.js';
|
|
9
|
+
import { certMatchesFingerprint, normalizeFingerprintHex, parseAgentMdTailSignature, publicKeyMatchesFingerprint, verifyDocument } from './cert-utils.js';
|
|
10
|
+
import { parseFileFrontmatter, validateAgentMdDocument, validateAgentMdReadDocument, verifyAgentMdBodyUpdate, verifyAgentMdUpload } from './agent-md-schema.js';
|
|
11
|
+
import { validateOwnershipFrontmatter, verifyOwnershipFrontmatterStatus } from './ownership-proof.js';
|
|
9
12
|
import { AUNError, ClientSignatureError, NotFoundError, StateError, ValidationError, } from './errors.js';
|
|
10
13
|
import { isJsonObject } from './types.js';
|
|
14
|
+
import { ownershipAid, ownershipFailure, parseOwnershipFile } from './ownership-v21.js';
|
|
15
|
+
import { resultOk } from './result.js';
|
|
11
16
|
const DEFAULT_HTTP_TIMEOUT_MS = 30_000;
|
|
17
|
+
function relationshipFields(meta) {
|
|
18
|
+
return Object.fromEntries(['owner', 'ownership', 'ownership_proof', 'ownership_release'].filter((key) => Object.hasOwn(meta, key)).map((key) => [key, meta[key]]));
|
|
19
|
+
}
|
|
20
|
+
function legacyRelationship(meta) {
|
|
21
|
+
if (['group', 'org'].includes(String(meta.type)))
|
|
22
|
+
return undefined;
|
|
23
|
+
const statement = meta.ownership?.statement;
|
|
24
|
+
if (statement && typeof statement === 'object' && Object.hasOwn(statement, 'relation_version'))
|
|
25
|
+
return undefined;
|
|
26
|
+
const fields = relationshipFields(meta);
|
|
27
|
+
return Object.keys(fields).length ? fields : undefined;
|
|
28
|
+
}
|
|
12
29
|
const AGENT_MD_NEGATIVE_CACHE_TTL_MS = 60_000;
|
|
30
|
+
export function buildOwnershipReleaseAgentMd(content, release) {
|
|
31
|
+
return updateOwnershipAgentMd(content, { ownership_release: release });
|
|
32
|
+
}
|
|
33
|
+
export function buildOwnershipAgentMd(content, owner, proof) {
|
|
34
|
+
return updateOwnershipAgentMd(content, { owner, ownership_proof: proof });
|
|
35
|
+
}
|
|
36
|
+
export function updateOwnershipAgentMd(content, fields) {
|
|
37
|
+
const payload = parseAgentMdTailSignature(String(content ?? '')).payload;
|
|
38
|
+
const match = /^---(?:\r\n|\n|\r)([\s\S]*?)(?:\r\n|\n|\r)---([\s\S]*)$/.exec(payload);
|
|
39
|
+
if (!match)
|
|
40
|
+
throw new ValidationError('agent.md must start with YAML frontmatter');
|
|
41
|
+
const document = parseDocument(match[1], { uniqueKeys: true });
|
|
42
|
+
if (document.errors.length)
|
|
43
|
+
throw new ValidationError(`agent.md frontmatter is invalid: ${document.errors[0].message}`);
|
|
44
|
+
const frontmatter = document.toJS();
|
|
45
|
+
if (!frontmatter || typeof frontmatter !== 'object' || Array.isArray(frontmatter))
|
|
46
|
+
throw new ValidationError('agent.md frontmatter must be a mapping');
|
|
47
|
+
document.delete('owner');
|
|
48
|
+
document.delete('ownership');
|
|
49
|
+
document.delete('ownership_proof');
|
|
50
|
+
document.delete('ownership_release');
|
|
51
|
+
for (const [name, value] of Object.entries(fields))
|
|
52
|
+
document.set(name, value);
|
|
53
|
+
const yamlText = document.toString().replace(/\r\n/g, '\n');
|
|
54
|
+
const updated = `---\n${yamlText.endsWith('\n') ? yamlText : `${yamlText}\n`}---${match[2]}`;
|
|
55
|
+
validateAgentMdDocument(updated, { expectedAid: String(frontmatter.aid ?? '') });
|
|
56
|
+
return updated;
|
|
57
|
+
}
|
|
13
58
|
export function buildDefaultAgentMd(aid, options = {}) {
|
|
14
59
|
const target = String(aid ?? '').trim();
|
|
15
60
|
const type = String(options.type ?? 'agent').trim() || 'agent';
|
|
@@ -22,6 +67,10 @@ export function buildDefaultAgentMd(aid, options = {}) {
|
|
|
22
67
|
`aid: ${JSON.stringify(target)}`,
|
|
23
68
|
`name: ${JSON.stringify(name)}`,
|
|
24
69
|
`type: ${JSON.stringify(type)}`,
|
|
70
|
+
...(options.exttype ? [`exttype: ${JSON.stringify(options.exttype)}`] : []),
|
|
71
|
+
...(options.owner ? [`owner: ${JSON.stringify(options.owner)}`] : []),
|
|
72
|
+
...(options.entityCertFingerprint ? [`entity_cert_fingerprint: ${JSON.stringify(options.entityCertFingerprint)}`] : []),
|
|
73
|
+
...(options.ownershipProof ? [`ownership_proof: ${JSON.stringify(options.ownershipProof)}`] : []),
|
|
25
74
|
'version: "1.0.0"',
|
|
26
75
|
`description: ${JSON.stringify(description)}`,
|
|
27
76
|
...(avatar ? [`avatar: ${JSON.stringify(avatar)}`] : []),
|
|
@@ -122,11 +171,13 @@ function requestText(url, opts) {
|
|
|
122
171
|
const chunks = [];
|
|
123
172
|
res.on('data', (chunk) => chunks.push(chunk));
|
|
124
173
|
res.on('end', () => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
headers: toHeaders(res.headers),
|
|
128
|
-
|
|
129
|
-
|
|
174
|
+
try {
|
|
175
|
+
const bytes = Buffer.concat(chunks);
|
|
176
|
+
resolve({ status, headers: toHeaders(res.headers), text: opts.strictUtf8 ? new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes) : bytes.toString('utf-8') });
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
reject(ownershipFailure(status >= 200 && status < 300 ? 'invalid' : 'unavailable', 'agent.md response is not valid UTF-8', status < 200 || status >= 300));
|
|
180
|
+
}
|
|
130
181
|
});
|
|
131
182
|
res.on('error', reject);
|
|
132
183
|
});
|
|
@@ -148,10 +199,14 @@ export class AgentMdManager {
|
|
|
148
199
|
_discoveryPort;
|
|
149
200
|
_uploadViaGatewayHost;
|
|
150
201
|
_ownerAidGetter;
|
|
202
|
+
_entityIdentityGetter;
|
|
203
|
+
_entityOwnerResolver;
|
|
204
|
+
_entitySignerResolver;
|
|
151
205
|
_currentAidGetter;
|
|
152
206
|
_gatewayResolver;
|
|
153
207
|
_peerResolver;
|
|
154
208
|
_accessTokenResolver;
|
|
209
|
+
_uploadAccessTokenResolver;
|
|
155
210
|
_aidValidator;
|
|
156
211
|
_cache = new Map();
|
|
157
212
|
_downloadInflight = new Map();
|
|
@@ -165,10 +220,14 @@ export class AgentMdManager {
|
|
|
165
220
|
this._discoveryPort = opts.discoveryPort ?? null;
|
|
166
221
|
this._uploadViaGatewayHost = opts.uploadViaGatewayHost ?? false;
|
|
167
222
|
this._ownerAidGetter = opts.ownerAidGetter;
|
|
223
|
+
this._entityIdentityGetter = opts.entityIdentityGetter;
|
|
224
|
+
this._entityOwnerResolver = opts.entityOwnerResolver;
|
|
225
|
+
this._entitySignerResolver = opts.entitySignerResolver;
|
|
168
226
|
this._currentAidGetter = opts.currentAidGetter;
|
|
169
227
|
this._gatewayResolver = opts.gatewayResolver;
|
|
170
228
|
this._peerResolver = opts.peerResolver;
|
|
171
229
|
this._accessTokenResolver = opts.accessTokenResolver;
|
|
230
|
+
this._uploadAccessTokenResolver = opts.uploadAccessTokenResolver;
|
|
172
231
|
this._aidValidator = opts.aidValidator;
|
|
173
232
|
}
|
|
174
233
|
static contentEtag(content) {
|
|
@@ -206,12 +265,12 @@ export class AgentMdManager {
|
|
|
206
265
|
this._atomicWriteText(filePath, String(content ?? ''));
|
|
207
266
|
return filePath;
|
|
208
267
|
}
|
|
209
|
-
|
|
268
|
+
_loadRecord(aid, strict = false) {
|
|
210
269
|
const target = String(aid ?? '').trim();
|
|
211
270
|
if (!target)
|
|
212
271
|
return null;
|
|
213
272
|
try {
|
|
214
|
-
const record = this._withRecordLock(target, () => this._readRecordUnlocked(target));
|
|
273
|
+
const record = this._withRecordLock(target, () => this._readRecordUnlocked(target, strict));
|
|
215
274
|
const loaded = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
|
|
216
275
|
try {
|
|
217
276
|
const content = this.readContent(target);
|
|
@@ -225,7 +284,7 @@ export class AgentMdManager {
|
|
|
225
284
|
}
|
|
226
285
|
if (Object.keys(loaded).length <= 1)
|
|
227
286
|
return null;
|
|
228
|
-
if (Object.keys(record).length === 0 && typeof loaded.content === 'string') {
|
|
287
|
+
if (Object.keys(record).length === 0 && typeof loaded.content === 'string' && !fs.existsSync(this.metaPath(target))) {
|
|
229
288
|
this._withRecordLock(target, () => this._writeRecordUnlocked(target, {
|
|
230
289
|
aid: target,
|
|
231
290
|
local_etag: loaded.local_etag,
|
|
@@ -236,11 +295,16 @@ export class AgentMdManager {
|
|
|
236
295
|
return { ...loaded };
|
|
237
296
|
}
|
|
238
297
|
catch (err) {
|
|
298
|
+
if (strict)
|
|
299
|
+
throw err;
|
|
239
300
|
this._log.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
240
301
|
return null;
|
|
241
302
|
}
|
|
242
303
|
}
|
|
243
|
-
|
|
304
|
+
loadRecord(aid) {
|
|
305
|
+
return this._loadRecord(aid);
|
|
306
|
+
}
|
|
307
|
+
_saveRecord(aid, fields, strict = false) {
|
|
244
308
|
const target = String(aid ?? '').trim();
|
|
245
309
|
if (!target)
|
|
246
310
|
return {};
|
|
@@ -249,6 +313,7 @@ export class AgentMdManager {
|
|
|
249
313
|
const hasContent = Object.prototype.hasOwnProperty.call(inputFields, 'content') && inputFields.content !== undefined && inputFields.content !== null;
|
|
250
314
|
let savedTo = '';
|
|
251
315
|
const record = this._withRecordLock(target, () => {
|
|
316
|
+
const next = { ...this._readRecordUnlocked(target, true), aid: target };
|
|
252
317
|
if (hasContent) {
|
|
253
318
|
const content = String(inputFields.content ?? '');
|
|
254
319
|
savedTo = this.writeContent(target, content);
|
|
@@ -258,7 +323,6 @@ export class AgentMdManager {
|
|
|
258
323
|
inputFields.fetched_at = Date.now();
|
|
259
324
|
}
|
|
260
325
|
delete inputFields.content;
|
|
261
|
-
const next = { ...this._readRecordUnlocked(target), aid: target };
|
|
262
326
|
for (const [key, value] of Object.entries(inputFields)) {
|
|
263
327
|
if (value !== undefined && value !== null)
|
|
264
328
|
next[key] = value;
|
|
@@ -274,7 +338,7 @@ export class AgentMdManager {
|
|
|
274
338
|
loaded.saved_to = savedTo;
|
|
275
339
|
}
|
|
276
340
|
else {
|
|
277
|
-
const current = this.
|
|
341
|
+
const current = this._loadRecord(target, strict);
|
|
278
342
|
if (current && typeof current.content === 'string')
|
|
279
343
|
loaded.content = current.content;
|
|
280
344
|
}
|
|
@@ -282,11 +346,27 @@ export class AgentMdManager {
|
|
|
282
346
|
return { ...loaded };
|
|
283
347
|
}
|
|
284
348
|
catch (err) {
|
|
349
|
+
if (strict)
|
|
350
|
+
throw err;
|
|
285
351
|
this._log.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
286
352
|
return {};
|
|
287
353
|
}
|
|
288
354
|
}
|
|
289
|
-
|
|
355
|
+
saveRecord(aid, fields) {
|
|
356
|
+
return this._saveRecord(aid, fields, Object.prototype.hasOwnProperty.call(fields, 'local_draft_content'));
|
|
357
|
+
}
|
|
358
|
+
_clearConfirmedDraft(aid, content) {
|
|
359
|
+
this._withRecordLock(aid, () => {
|
|
360
|
+
const record = this._readRecordUnlocked(aid, true);
|
|
361
|
+
if (record.local_draft_content !== content)
|
|
362
|
+
return;
|
|
363
|
+
record.local_draft_content = '';
|
|
364
|
+
record.updated_at = Date.now();
|
|
365
|
+
this._writeRecordUnlocked(aid, record);
|
|
366
|
+
});
|
|
367
|
+
this._cache.delete(aid);
|
|
368
|
+
}
|
|
369
|
+
async upload(content) {
|
|
290
370
|
const target = this._ownerAid();
|
|
291
371
|
if (!target)
|
|
292
372
|
throw new ValidationError('uploadAgentMd requires local AID');
|
|
@@ -294,26 +374,486 @@ export class AgentMdManager {
|
|
|
294
374
|
if (!current?.isPrivateKeyValid()) {
|
|
295
375
|
throw new StateError('uploadAgentMd requires loaded AID with a valid private key');
|
|
296
376
|
}
|
|
297
|
-
const rawContent = content === undefined || content === null ? this.readContent(target) : String(content);
|
|
377
|
+
const rawContent = content === undefined || content === null ? String(this._loadRecord(target, true)?.local_draft_content ?? '') || this.readContent(target) : String(content);
|
|
298
378
|
if (!rawContent.trim())
|
|
299
379
|
throw new ValidationError('uploadAgentMd requires non-empty content');
|
|
300
|
-
validateAgentMdDocument(rawContent, { expectedAid: target });
|
|
380
|
+
const parsed = validateAgentMdDocument(rawContent, { expectedAid: target, validateOwnership: false });
|
|
381
|
+
if (['group', 'org'].includes(String(parsed.frontmatter.type)))
|
|
382
|
+
return await this.uploadAs(target, rawContent, current);
|
|
383
|
+
if (parseAgentMdTailSignature(rawContent).signatures.length > 1) {
|
|
384
|
+
await this._verifyDocument(rawContent, target, current, true, true);
|
|
385
|
+
return await this._uploadPrepared(target, rawContent, current);
|
|
386
|
+
}
|
|
301
387
|
const signed = current.signAgentMd(rawContent);
|
|
302
388
|
if (!signed.ok || !signed.data) {
|
|
303
389
|
const message = signed.error?.message ?? 'agent.md signing failed';
|
|
304
390
|
throw new ClientSignatureError(message);
|
|
305
391
|
}
|
|
306
392
|
const signedContent = signed.data.signed;
|
|
307
|
-
|
|
393
|
+
verifyAgentMdBodyUpdate(signedContent, current.certPem, target);
|
|
394
|
+
return await this._uploadPrepared(target, signedContent, current);
|
|
395
|
+
}
|
|
396
|
+
async readOwnershipFresh(aid) {
|
|
397
|
+
const target = this.safeAid(aid);
|
|
398
|
+
const response = await this._ownershipRequest(target, 'GET');
|
|
399
|
+
if (response.status !== 200 && response.status !== 404)
|
|
400
|
+
throw ownershipFailure('unavailable', `NS fresh read failed: HTTP ${response.status}`, true);
|
|
401
|
+
if (headerValue(response.headers, 'X-AUN-Ownership-Status') === 'not_applicable')
|
|
402
|
+
throw ownershipFailure('not_applicable', 'group/org uses its lifecycle owner relationship');
|
|
403
|
+
let type;
|
|
404
|
+
if (response.status === 200) {
|
|
405
|
+
try {
|
|
406
|
+
type = parseFileFrontmatter(parseAgentMdTailSignature(response.text).payload).type;
|
|
407
|
+
}
|
|
408
|
+
catch { }
|
|
409
|
+
}
|
|
410
|
+
if (['group', 'org'].includes(String(type)))
|
|
411
|
+
throw ownershipFailure('not_applicable', 'group/org uses its lifecycle owner relationship');
|
|
412
|
+
const protocol = headerValue(response.headers, 'X-AUN-Ownership-Protocol');
|
|
413
|
+
if (protocol !== '2.1')
|
|
414
|
+
throw ownershipFailure('unsupported_protocol', 'NS does not provide Ownership 2.1 metadata');
|
|
415
|
+
const version = headerValue(response.headers, 'X-AUN-Ownership-Version');
|
|
416
|
+
const lock = headerValue(response.headers, 'X-AUN-Ownership-Lock');
|
|
417
|
+
const etag = headerValue(response.headers, 'ETag');
|
|
418
|
+
if (!/^(?:0|[1-9][0-9]*)$/.test(version) || !Number.isSafeInteger(Number(version)) || !['locked', 'unlocked'].includes(lock) || response.status === 200 && !etag || response.status === 404 && (version !== '0' || lock !== 'unlocked'))
|
|
419
|
+
throw ownershipFailure('unavailable', 'NS fresh metadata is incomplete', true);
|
|
420
|
+
return { sub_aid: target, exists: response.status === 200, content: response.status === 200 ? response.text : '', etag, protocol, relation_version: Number(version), lock: lock, observed_at: Math.floor(Date.now() / 1000) };
|
|
421
|
+
}
|
|
422
|
+
async submitOwnershipRaw(aid, raw, condition) {
|
|
423
|
+
const target = this.safeAid(aid), current = this._currentAid();
|
|
424
|
+
if (!current?.isPrivateKeyValid() || current.aid !== target || this._ownerAid() !== target)
|
|
425
|
+
throw ownershipFailure('forbidden', 'raw agent.md submission requires its own authenticated identity');
|
|
426
|
+
if (!condition || !condition.if_match && condition.if_none_match !== '*' || condition.if_match && condition.if_none_match)
|
|
427
|
+
throw new ValidationError('raw agent.md requires one original condition');
|
|
428
|
+
parseOwnershipFile(raw, 'agent', { expectedSubAid: target });
|
|
429
|
+
verifyAgentMdUpload(raw, current.certPem, target);
|
|
430
|
+
const response = await this._ownershipRequest(target, 'PUT', raw, condition);
|
|
431
|
+
if (response.status === 409 || response.status === 412)
|
|
432
|
+
throw ownershipFailure('conflict', `NS original condition failed: HTTP ${response.status}`);
|
|
433
|
+
if (response.status === 428)
|
|
434
|
+
throw ownershipFailure('unsupported_protocol', 'NS does not support the required original document condition');
|
|
435
|
+
if (response.status === 401 || response.status === 403)
|
|
436
|
+
throw ownershipFailure('forbidden', `NS self submission denied: HTTP ${response.status}`);
|
|
437
|
+
if (response.status === 400)
|
|
438
|
+
throw ownershipFailure('invalid', 'NS rejected the submitted agent.md: HTTP 400');
|
|
439
|
+
if (response.status < 200 || response.status >= 300)
|
|
440
|
+
throw ownershipFailure('unavailable', `NS raw submission failed: HTTP ${response.status}`, true);
|
|
441
|
+
const fresh = await this.readOwnershipFresh(target);
|
|
442
|
+
if (fresh.content !== raw)
|
|
443
|
+
throw ownershipFailure('unavailable', 'NS raw submission cannot be confirmed from the current document', true);
|
|
444
|
+
this.saveRecord(target, { content: raw, local_etag: AgentMdManager.contentEtag(raw), remote_etag: fresh.etag, fetched_at: Date.now(), checked_at: Date.now(), remote_status: 'found' });
|
|
445
|
+
return fresh;
|
|
446
|
+
}
|
|
447
|
+
async _ownershipRequest(target, method, raw, condition, legacyRead = false) {
|
|
448
|
+
const gatewayUrl = await this._resolveGateway(target);
|
|
449
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown', 'Cache-Control': 'no-cache', ...(!legacyRead ? { 'X-AUN-Ownership-Check': '1' } : {}) };
|
|
450
|
+
if (method === 'PUT') {
|
|
451
|
+
headers['Content-Type'] = 'text/markdown; charset=utf-8';
|
|
452
|
+
headers.Authorization = `Bearer ${await this._uploadAccessToken(target, gatewayUrl)}`;
|
|
453
|
+
if (condition?.if_match)
|
|
454
|
+
headers['If-Match'] = condition.if_match;
|
|
455
|
+
else if (condition?.if_none_match)
|
|
456
|
+
headers['If-None-Match'] = condition.if_none_match;
|
|
457
|
+
}
|
|
458
|
+
const viaGateway = this._uploadViaGatewayHost;
|
|
459
|
+
if (viaGateway)
|
|
460
|
+
headers.Host = target;
|
|
461
|
+
const request = (url) => requestText(url, { method, verifySsl: this._verifySsl, headers, body: raw, timeoutMs: DEFAULT_HTTP_TIMEOUT_MS, redirectsLeft: 0, strictUtf8: true });
|
|
462
|
+
let response = await request(this._url(target, gatewayUrl, viaGateway));
|
|
463
|
+
if (response.status === 404 && !headerValue(response.headers, 'X-AUN-Ownership-Protocol') && headerValue(response.headers, 'X-AUN-Ownership-Status') !== 'not_applicable') {
|
|
464
|
+
const fallback = nameServiceAgentMdUrl(target, gatewayUrl);
|
|
465
|
+
if (fallback && fallback !== this._url(target, gatewayUrl, viaGateway)) {
|
|
466
|
+
headers.Host = target;
|
|
467
|
+
response = await request(fallback);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return response;
|
|
471
|
+
}
|
|
472
|
+
async uploadAs(targetAid, content, signer) {
|
|
473
|
+
const target = this.safeAid(String(targetAid ?? '').trim());
|
|
474
|
+
const rawContent = String(content ?? '');
|
|
475
|
+
if (!rawContent.trim())
|
|
476
|
+
throw new ValidationError('uploadAgentMd requires non-empty content');
|
|
477
|
+
const parsed = validateAgentMdDocument(rawContent, { expectedAid: target, validateOwnership: false });
|
|
478
|
+
if (['group', 'org'].includes(String(parsed.frontmatter.type)) && parsed.signature) {
|
|
479
|
+
const identity = await this._entitySigningIdentity(target, parsed.frontmatter, parsed.signature, signer);
|
|
480
|
+
await this._verifyDocument(rawContent, target, identity, true);
|
|
481
|
+
return await this._uploadPrepared(target, rawContent, identity);
|
|
482
|
+
}
|
|
483
|
+
if (['group', 'org'].includes(String(parsed.frontmatter.type)) && signer.aid !== target) {
|
|
484
|
+
const identity = await this._entityIdentityGetter?.(target, String(parsed.frontmatter.type));
|
|
485
|
+
if (!identity?.isPrivateKeyValid())
|
|
486
|
+
throw new StateError('entity agent.md signing requires its local private key');
|
|
487
|
+
signer = identity;
|
|
488
|
+
}
|
|
489
|
+
if (!signer?.isPrivateKeyValid())
|
|
490
|
+
throw new StateError('uploadAgentMd requires a valid signer identity');
|
|
491
|
+
if (parseAgentMdTailSignature(rawContent).signatures.length > 1) {
|
|
492
|
+
await this._verifyDocument(rawContent, target, signer, true, true);
|
|
493
|
+
return await this._uploadPrepared(target, rawContent, signer);
|
|
494
|
+
}
|
|
495
|
+
const signed = signer.signAgentMd(rawContent);
|
|
496
|
+
if (!signed.ok || !signed.data) {
|
|
497
|
+
const message = signed.error?.message ?? 'agent.md signing failed';
|
|
498
|
+
throw new ClientSignatureError(message);
|
|
499
|
+
}
|
|
500
|
+
const signedContent = signed.data.signed;
|
|
501
|
+
verifyAgentMdBodyUpdate(signedContent, signer.certPem, target);
|
|
502
|
+
return await this._uploadPrepared(target, signedContent, signer);
|
|
503
|
+
}
|
|
504
|
+
_pendingUpload(aid, strict = false) {
|
|
505
|
+
const record = this._loadRecord(aid, strict);
|
|
506
|
+
if (record?.pending_operation !== 'replace' || !String(record.pending_content ?? '').trim())
|
|
507
|
+
return null;
|
|
508
|
+
return { content: String(record.pending_content), content_etag: String(record.pending_content_etag ?? ''), last_error: String(record.pending_error ?? ''), condition: record.pending_condition, relation: record.pending_relation, authorization: record.pending_authorization };
|
|
509
|
+
}
|
|
510
|
+
pendingUpload(aid) {
|
|
511
|
+
return this._pendingUpload(aid, true);
|
|
512
|
+
}
|
|
513
|
+
async resumePendingUpload(aid, signer) {
|
|
514
|
+
const pending = this.pendingUpload(aid);
|
|
515
|
+
if (!pending)
|
|
516
|
+
return false;
|
|
517
|
+
const condition = pending.condition;
|
|
518
|
+
if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
|
|
519
|
+
throw ownershipFailure('persistence_unavailable', 'pending agent.md requires its original document condition');
|
|
520
|
+
const parsed = validateAgentMdDocument(pending.content, { expectedAid: aid, validateOwnership: false });
|
|
521
|
+
const meta = parsed.frontmatter;
|
|
522
|
+
try {
|
|
523
|
+
if (pending.relation?.legacy === true)
|
|
524
|
+
return await this._resumeLegacyBodyUpload(aid, pending, signer ?? this._currentAid());
|
|
525
|
+
if (legacyRelationship(meta))
|
|
526
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership pending agent.md requires its original relationship snapshot');
|
|
527
|
+
if (['group', 'org'].includes(String(meta.type))) {
|
|
528
|
+
if (!parsed.signature)
|
|
529
|
+
throw ownershipFailure('invalid', 'pending entity agent.md requires a signature');
|
|
530
|
+
const identity = await this._entitySigningIdentity(aid, meta, parsed.signature, signer ?? this._currentAid() ?? undefined);
|
|
531
|
+
const context = await this._entityContext(aid, meta, identity);
|
|
532
|
+
if (!pending.relation?.entity_cert_fingerprint)
|
|
533
|
+
throw ownershipFailure('persistence_unavailable', 'pending entity upload requires original owner and certificate context');
|
|
534
|
+
if (pending.relation.owner_aid !== context.owner_aid)
|
|
535
|
+
throw ownershipFailure('forbidden', 'pending entity upload belongs to a different owner');
|
|
536
|
+
if (pending.relation.entity_cert_fingerprint !== context.entity_cert_fingerprint)
|
|
537
|
+
throw ownershipFailure('conflict', 'pending entity upload belongs to an old rekey generation');
|
|
538
|
+
if (pending.content_etag !== AgentMdManager.contentEtag(pending.content))
|
|
539
|
+
throw ownershipFailure('persistence_unavailable', 'pending entity original bytes are corrupt');
|
|
540
|
+
await this._verifyDocument(pending.content, aid, identity, true);
|
|
541
|
+
const remote = await this._readEntityFresh(aid);
|
|
542
|
+
if (remote.exists && remote.content === pending.content && remote.etag === pending.content_etag) {
|
|
543
|
+
const saved = this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
544
|
+
if (!saved.aid)
|
|
545
|
+
throw new StateError('agent.md pending completion persistence failed');
|
|
546
|
+
this._clearConfirmedDraft(aid, pending.content);
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
|
|
550
|
+
throw ownershipFailure('conflict', 'pending agent.md original document condition changed');
|
|
551
|
+
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
const multipleSignatures = parseAgentMdTailSignature(pending.content).signatures.length > 1;
|
|
555
|
+
const identity = signer ?? this._currentAid();
|
|
556
|
+
if (multipleSignatures) {
|
|
557
|
+
if (!identity)
|
|
558
|
+
throw new StateError('pending agent.md requires a signer identity');
|
|
559
|
+
await this._verifyDocument(pending.content, aid, identity, true);
|
|
560
|
+
}
|
|
561
|
+
const remote = await this.readOwnershipFresh(aid);
|
|
562
|
+
const fail = (code, reason, retryable = false) => ownershipFailure(code, reason, retryable);
|
|
563
|
+
const candidate = parseOwnershipFile(pending.content, 'agent', { expectedSubAid: aid });
|
|
564
|
+
const relation = pending.relation && Object.keys(pending.relation).length ? pending.relation : undefined;
|
|
565
|
+
const sameStatement = (first, second) => first == null && second == null || first !== null && second !== null && typeof first === 'object' && typeof second === 'object' && ['version', 'declaration_id', 'owner_aid', 'sub_aid', 'relation_version', 'declared_at'].every((field) => first[field] === second[field]);
|
|
566
|
+
if (candidate.statement && !relation)
|
|
567
|
+
throw fail('persistence_unavailable', 'pending ownership body update requires its original relationship context');
|
|
568
|
+
if (relation) {
|
|
569
|
+
if (remote.relation_version !== relation.relation_version || remote.lock !== relation.lock)
|
|
570
|
+
throw fail('conflict', 'pending agent.md original relationship changed');
|
|
571
|
+
if (!sameStatement(candidate.statement, relation.statement))
|
|
572
|
+
throw fail('unsupported_protocol', 'ownership changes require their persisted 2.1 task');
|
|
573
|
+
if (remote.exists && (remote.relation_version > 0 || candidate.statement)) {
|
|
574
|
+
const current = parseOwnershipFile(remote.content, 'agent', { expectedSubAid: aid });
|
|
575
|
+
if (!sameStatement(current.statement, relation.statement) || (remote.owner_aid ?? current.statement?.owner_aid ?? null) !== relation.owner_aid)
|
|
576
|
+
throw fail('conflict', 'pending agent.md relationship statement changed');
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (!relation && (remote.relation_version !== 0 || remote.lock !== 'unlocked'))
|
|
580
|
+
throw fail('unavailable', 'pending agent.md cannot overwrite relationship history', true);
|
|
581
|
+
if (remote.exists && remote.etag === pending.content_etag && remote.content === pending.content) {
|
|
582
|
+
const saved = this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
583
|
+
if (!saved.aid)
|
|
584
|
+
throw new StateError('agent.md pending completion persistence failed');
|
|
585
|
+
this._clearConfirmedDraft(aid, pending.content);
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
|
|
589
|
+
throw ownershipFailure('conflict', 'pending agent.md original document condition changed');
|
|
590
|
+
if (!identity)
|
|
591
|
+
throw new StateError('pending agent.md requires a signer identity');
|
|
592
|
+
if (!multipleSignatures)
|
|
593
|
+
verifyAgentMdUpload(pending.content, identity.certPem, aid);
|
|
594
|
+
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
this.saveRecord(aid, { pending_error: error instanceof Error ? error.message : String(error) });
|
|
599
|
+
throw error;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async _uploadPrepared(target, signedContent, signer, originalCondition) {
|
|
603
|
+
const meta = validateAgentMdDocument(signedContent, { expectedAid: target, validateOwnership: false }).frontmatter;
|
|
604
|
+
const entity = ['group', 'org'].includes(String(meta.type));
|
|
605
|
+
const legacy = legacyRelationship(meta);
|
|
606
|
+
const draftStrict = !originalCondition;
|
|
607
|
+
if (legacy && (signer.aid !== target || this._currentAid()?.aid !== target || this._ownerAid() !== target))
|
|
608
|
+
throw ownershipFailure('forbidden', 'legacy relationship body updates require current subject identity');
|
|
609
|
+
if (legacy && Object.hasOwn(meta, 'owner'))
|
|
610
|
+
validateOwnershipFrontmatter({ aid: meta.aid, type: meta.type, owner: meta.owner }, target);
|
|
611
|
+
if (!entity && !legacy)
|
|
612
|
+
parseOwnershipFile(signedContent, 'agent', { expectedSubAid: target });
|
|
613
|
+
if (draftStrict && this._saveRecord(target, { local_draft_content: signedContent }, true).local_draft_content !== signedContent)
|
|
614
|
+
throw new StateError('agent.md local draft persistence failed');
|
|
615
|
+
const entityContext = entity ? await this._entityContext(target, meta, signer) : undefined;
|
|
616
|
+
let condition = originalCondition;
|
|
617
|
+
let relation = entityContext ?? (this._pendingUpload(target, draftStrict))?.relation;
|
|
618
|
+
if (!condition) {
|
|
619
|
+
const fresh = entity ? await this._readEntityFresh(target) : legacy ? await this._readLegacyBodyBase(target) : await this.readOwnershipFresh(target);
|
|
620
|
+
condition = fresh.exists ? { if_match: fresh.etag } : { if_none_match: '*' };
|
|
621
|
+
if (!entity) {
|
|
622
|
+
if (legacy) {
|
|
623
|
+
if (!fresh.exists || !fresh.etag || !isDeepStrictEqual(legacy, legacyRelationship(validateAgentMdReadDocument(fresh.content, { expectedAid: target }).frontmatter)))
|
|
624
|
+
throw ownershipFailure('unsupported_protocol', 'legacy relationship material must remain unchanged');
|
|
625
|
+
relation = { legacy: true, base_content: fresh.content, base_content_etag: AgentMdManager.contentEtag(fresh.content), base_etag: fresh.etag, fields: legacy };
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
if (fresh.exists && legacyRelationship(validateAgentMdReadDocument(fresh.content, { expectedAid: target }).frontmatter))
|
|
629
|
+
throw ownershipFailure('unsupported_protocol', 'legacy relationship material cannot be removed or replaced');
|
|
630
|
+
const authority = fresh;
|
|
631
|
+
const candidate = parseOwnershipFile(signedContent, 'agent', { expectedSubAid: target });
|
|
632
|
+
const statement = fresh.exists && (authority.relation_version > 0 || candidate.statement) ? parseOwnershipFile(fresh.content, 'agent', { expectedSubAid: target }).statement : undefined;
|
|
633
|
+
relation = { relation_version: authority.relation_version, lock: authority.lock, owner_aid: authority.owner_aid ?? statement?.owner_aid ?? null, statement: statement ?? null };
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const uploader = this._currentAid();
|
|
638
|
+
const entityAuthorization = '';
|
|
639
|
+
const pending = this._saveRecord(target, { pending_operation: 'replace', pending_target_aid: target, pending_content: signedContent, pending_content_etag: AgentMdManager.contentEtag(signedContent), pending_error: '', pending_condition: condition, pending_relation: relation, pending_authorization: entityAuthorization }, draftStrict);
|
|
640
|
+
const savedCondition = pending.pending_condition;
|
|
641
|
+
if (pending.pending_content !== signedContent || savedCondition?.if_match !== condition.if_match || savedCondition?.if_none_match !== condition.if_none_match || pending.pending_authorization !== entityAuthorization || legacy && !isDeepStrictEqual(pending.pending_relation, relation))
|
|
642
|
+
throw new StateError('agent.md pending upload persistence failed');
|
|
643
|
+
try {
|
|
644
|
+
const owner = this._currentAid();
|
|
645
|
+
const delegated = target.toLowerCase() !== this._ownerAid().toLowerCase();
|
|
646
|
+
if (delegated && !owner?.isPrivateKeyValid())
|
|
647
|
+
throw new StateError('owner agent.md upload requires a loaded owner identity');
|
|
648
|
+
const authorization = entity ? entityAuthorization : delegated ? this._ownerUploadAuthorization(target, signedContent, owner) : '';
|
|
649
|
+
const result = await this._uploadHttp(target, signedContent, authorization, condition);
|
|
650
|
+
const remoteEtag = String(result.etag ?? '').trim();
|
|
651
|
+
const saved = this._saveRecord(target, {
|
|
652
|
+
content: signedContent, local_etag: AgentMdManager.contentEtag(signedContent), remote_etag: remoteEtag || undefined,
|
|
653
|
+
last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(), fetched_at: Date.now(), checked_at: Date.now(),
|
|
654
|
+
remote_status: remoteEtag ? 'found' : 'unknown', last_error: '',
|
|
655
|
+
pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '',
|
|
656
|
+
}, draftStrict);
|
|
657
|
+
if (!saved.aid)
|
|
658
|
+
throw new StateError('agent.md upload completion persistence failed');
|
|
659
|
+
if (remoteEtag === AgentMdManager.contentEtag(signedContent))
|
|
660
|
+
this._clearConfirmedDraft(target, signedContent);
|
|
661
|
+
return { ...result };
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
this._saveRecord(target, { pending_error: error instanceof Error ? error.message : String(error) }, draftStrict);
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async _verifyLegacyBodyContent(target, content) {
|
|
669
|
+
const parsed = validateAgentMdReadDocument(content, { expectedAid: target, requireSignature: true });
|
|
670
|
+
if (!legacyRelationship(parsed.frontmatter))
|
|
671
|
+
throw ownershipFailure('unsupported_protocol', 'stored agent.md does not contain a legacy relationship');
|
|
672
|
+
const peer = await this._resolvePeer(target, parsed.signature.cert_fingerprint, Number(parsed.signature.timestamp));
|
|
673
|
+
const verified = await this._verifyDocument(content, target, peer);
|
|
674
|
+
if (verified.status !== 'verified')
|
|
675
|
+
throw ownershipFailure('invalid', 'stored legacy agent.md signature is invalid');
|
|
676
|
+
return parsed;
|
|
677
|
+
}
|
|
678
|
+
async _readLegacyBodyBase(target) {
|
|
679
|
+
const response = await this._ownershipRequest(target, 'GET', undefined, undefined, true);
|
|
680
|
+
if (response.status === 404)
|
|
681
|
+
return { exists: false, content: '', etag: '' };
|
|
682
|
+
if (response.status !== 200)
|
|
683
|
+
throw ownershipFailure('unavailable', `legacy agent.md fresh read failed: HTTP ${response.status}`, true);
|
|
684
|
+
await this._verifyLegacyBodyContent(target, response.text);
|
|
685
|
+
const etag = headerValue(response.headers, 'ETag');
|
|
686
|
+
if (!etag)
|
|
687
|
+
throw ownershipFailure('unavailable', 'legacy agent.md original ETag is missing', true);
|
|
688
|
+
return { exists: true, content: response.text, etag };
|
|
689
|
+
}
|
|
690
|
+
async _resumeLegacyBodyUpload(target, pending, identity) {
|
|
691
|
+
const relation = pending.relation;
|
|
692
|
+
if (!identity || identity.aid !== target || this._currentAid()?.aid !== target || this._ownerAid() !== target)
|
|
693
|
+
throw ownershipFailure('forbidden', 'legacy relationship recovery requires current subject identity');
|
|
694
|
+
if (typeof relation.base_content !== 'string' || relation.base_content_etag !== AgentMdManager.contentEtag(relation.base_content) || typeof relation.base_etag !== 'string' || !relation.base_etag || pending.condition?.if_match !== relation.base_etag || pending.content_etag !== AgentMdManager.contentEtag(pending.content))
|
|
695
|
+
throw ownershipFailure('persistence_unavailable', 'pending legacy relationship original bytes or condition are missing');
|
|
696
|
+
if (parseAgentMdTailSignature(pending.content).signatures.length > 1)
|
|
697
|
+
await this._verifyDocument(pending.content, target, identity, true, true);
|
|
698
|
+
else
|
|
699
|
+
verifyAgentMdBodyUpdate(pending.content, identity.certPem, target);
|
|
700
|
+
const base = await this._verifyLegacyBodyContent(target, relation.base_content);
|
|
701
|
+
const candidate = validateAgentMdDocument(pending.content, { expectedAid: target, validateOwnership: false });
|
|
702
|
+
if (!isDeepStrictEqual(legacyRelationship(base.frontmatter), relation.fields) || !isDeepStrictEqual(relationshipFields(candidate.frontmatter), relation.fields))
|
|
703
|
+
throw ownershipFailure('persistence_unavailable', 'pending legacy relationship material changed');
|
|
704
|
+
const remote = await this._readLegacyBodyBase(target);
|
|
705
|
+
if (!remote.exists)
|
|
706
|
+
throw ownershipFailure('conflict', 'pending legacy agent.md original document is missing');
|
|
707
|
+
if (remote.content === pending.content && remote.etag === pending.content_etag) {
|
|
708
|
+
const saved = this.saveRecord(target, { content: pending.content, remote_etag: remote.etag, pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
709
|
+
if (!saved.aid)
|
|
710
|
+
throw new StateError('agent.md pending completion persistence failed');
|
|
711
|
+
this._clearConfirmedDraft(target, pending.content);
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
if (remote.content !== relation.base_content || remote.etag !== relation.base_etag)
|
|
715
|
+
throw ownershipFailure('conflict', 'pending legacy agent.md original document changed');
|
|
716
|
+
await this._uploadPrepared(target, pending.content, identity, pending.condition);
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
async _readEntityFresh(target) {
|
|
720
|
+
const gateway = await this._resolveGateway(target);
|
|
721
|
+
const headers = { 'X-AUN-Ownership-Check': '1', 'X-AUN-Signature-Format': 'multi-v1', 'Cache-Control': 'no-cache', ...(this._uploadViaGatewayHost ? { Host: target } : {}) };
|
|
722
|
+
const request = (url) => requestText(url, { method: 'GET', verifySsl: this._verifySsl, headers, timeoutMs: DEFAULT_HTTP_TIMEOUT_MS, redirectsLeft: 0, strictUtf8: true });
|
|
723
|
+
const response = await request(this._url(target, gateway, this._uploadViaGatewayHost));
|
|
724
|
+
if (response.status !== 200 && response.status !== 404)
|
|
725
|
+
throw ownershipFailure('unavailable', `entity agent.md fresh read failed: HTTP ${response.status}`, true);
|
|
726
|
+
if (headerValue(response.headers, 'X-AUN-Ownership-Status') !== 'not_applicable' || !['current', 'stale'].includes(headerValue(response.headers, 'X-AUN-Entity-Verification-Status')))
|
|
727
|
+
throw ownershipFailure('unavailable', 'entity authority status metadata is unavailable', true);
|
|
728
|
+
const etag = headerValue(response.headers, 'ETag');
|
|
729
|
+
if (response.status === 200 && !etag)
|
|
730
|
+
throw ownershipFailure('unavailable', 'entity agent.md fresh read has no ETag', true);
|
|
731
|
+
return { exists: response.status === 200, content: response.status === 200 ? response.text : '', etag, owner_aid: headerValue(response.headers, 'X-AUN-Entity-Owner-AID'), entity_cert_fingerprint: headerValue(response.headers, 'X-AUN-Entity-Cert-Fingerprint') };
|
|
732
|
+
}
|
|
733
|
+
async readEntityAuthorityFresh(target) {
|
|
734
|
+
const observed = await this._readEntityFresh(this.safeAid(target));
|
|
735
|
+
try {
|
|
736
|
+
ownershipAid(observed.owner_aid, 'owner_aid');
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
throw ownershipFailure('unavailable', 'entity owner authority metadata is unavailable', true);
|
|
740
|
+
}
|
|
741
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(observed.entity_cert_fingerprint))
|
|
742
|
+
throw ownershipFailure('unavailable', 'entity owner authority metadata is unavailable', true);
|
|
743
|
+
return { owner_aid: observed.owner_aid, entity_cert_fingerprint: observed.entity_cert_fingerprint };
|
|
744
|
+
}
|
|
745
|
+
async _entityContext(target, meta, signer) {
|
|
746
|
+
const owner = String(meta.owner ?? '').trim().toLowerCase();
|
|
747
|
+
const uploader = this._currentAid();
|
|
748
|
+
if (signer.aid.toLowerCase() !== target.toLowerCase() || !uploader || uploader.aid.toLowerCase() !== owner || uploader.aid.toLowerCase() !== this._ownerAid().toLowerCase())
|
|
749
|
+
throw ownershipFailure('forbidden', 'group/org agent.md requires current owner identity');
|
|
750
|
+
const context = await this._entityCurrentContext(target, meta, true);
|
|
751
|
+
if (signer.certFingerprint !== context.entity_cert_fingerprint)
|
|
752
|
+
throw ownershipFailure('conflict', 'entity signing certificate changed');
|
|
753
|
+
if (uploader.aid.toLowerCase() === target.toLowerCase()) {
|
|
754
|
+
if (uploader.certFingerprint !== context.entity_cert_fingerprint)
|
|
755
|
+
throw ownershipFailure('conflict', 'entity uploader certificate changed');
|
|
756
|
+
}
|
|
757
|
+
else if (uploader.certFingerprint !== signer.certFingerprint)
|
|
758
|
+
await this._verifyEntityOwnerSigner(uploader);
|
|
759
|
+
return context;
|
|
760
|
+
}
|
|
761
|
+
async _entitySigningIdentity(target, meta, signature, candidate) {
|
|
762
|
+
const signer = signature.signer_aid ?? target;
|
|
763
|
+
if (candidate?.aid === signer && candidate.certFingerprint === signature.cert_fingerprint)
|
|
764
|
+
return candidate;
|
|
765
|
+
if (signer === target)
|
|
766
|
+
return await this._entityIdentityGetter?.(target, String(meta.type)) ?? await this._resolvePeer(target);
|
|
767
|
+
if (!this._entitySignerResolver)
|
|
768
|
+
throw ownershipFailure('unavailable', 'entity owner signing certificate resolver is unavailable', true);
|
|
769
|
+
return await this._entitySignerResolver(signer, signature.cert_fingerprint);
|
|
770
|
+
}
|
|
771
|
+
async _verifyDocument(content, target, identity, current = false, bodyUpdate = false) {
|
|
772
|
+
const certificates = new Map();
|
|
773
|
+
const parsed = parseAgentMdTailSignature(content, !current);
|
|
774
|
+
const meta = validateAgentMdReadDocument(content).frontmatter;
|
|
775
|
+
const readCompatibility = !current && (!['group', 'org'].includes(String(meta.type)) || meta.type === 'group' && !Object.hasOwn(meta, 'entity_cert_fingerprint') && parsed.signatures.length === 1 && (parsed.fields?.signer_aid === undefined || parsed.fields.signer_aid === meta.aid));
|
|
776
|
+
for (const item of parsed.signatures.slice(1)) {
|
|
777
|
+
const fields = item.fields;
|
|
778
|
+
const peer = current && this._entitySignerResolver ? await this._entitySignerResolver(fields.signer_aid, fields.cert_fingerprint) : await this._resolvePeer(fields.signer_aid, fields.cert_fingerprint, Number(fields.timestamp));
|
|
779
|
+
certificates.set(`${fields.signer_aid};${fields.cert_fingerprint}`, peer.certPem);
|
|
780
|
+
}
|
|
781
|
+
const resolver = (aid, fingerprint) => certificates.get(`${aid};${fingerprint}`);
|
|
782
|
+
if (current)
|
|
783
|
+
(bodyUpdate ? verifyAgentMdBodyUpdate : verifyAgentMdUpload)(content, identity.certPem, target, undefined, resolver);
|
|
784
|
+
const result = verifyDocument(content, { expectedAid: target, primaryCertPem: identity.certPem, requireCurrentValidity: current, readCompatibility, certificateResolver: resolver });
|
|
785
|
+
if (result.status === 'invalid')
|
|
786
|
+
throw ownershipFailure('invalid', result.reason ?? 'document signature verification failed');
|
|
787
|
+
return result;
|
|
788
|
+
}
|
|
789
|
+
async _verifyEntityOwnerSigner(owner) {
|
|
790
|
+
if (!this._entitySignerResolver)
|
|
791
|
+
throw ownershipFailure('unavailable', 'entity owner signing certificate resolver is unavailable', true);
|
|
792
|
+
const fresh = await this._entitySignerResolver(owner.aid, owner.certFingerprint);
|
|
793
|
+
if (fresh.aid.toLowerCase() !== owner.aid.toLowerCase() || fresh.certFingerprint !== owner.certFingerprint) {
|
|
794
|
+
throw ownershipFailure('conflict', 'entity owner signing certificate changed');
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async _entityCurrentContext(target, meta, uploading = false) {
|
|
798
|
+
const owner = String(meta.owner ?? '').trim().toLowerCase();
|
|
799
|
+
const observed = this._entityOwnerResolver ? null : await this._readEntityFresh(target);
|
|
800
|
+
if (observed && !/^sha256:[0-9a-f]{64}$/.test(observed.entity_cert_fingerprint))
|
|
801
|
+
throw ownershipFailure('unavailable', 'entity authority metadata is unavailable', true);
|
|
802
|
+
const freshOwner = String(this._entityOwnerResolver ? await this._entityOwnerResolver(target, String(meta.type)) : observed?.owner_aid).trim().toLowerCase();
|
|
803
|
+
if (!freshOwner)
|
|
804
|
+
throw ownershipFailure('unavailable', 'entity current owner is unavailable', true);
|
|
805
|
+
if (freshOwner !== owner)
|
|
806
|
+
throw ownershipFailure(uploading ? 'forbidden' : 'conflict', 'entity current owner changed');
|
|
807
|
+
const entity = await this._entityIdentityGetter?.(target, String(meta.type)) ?? await this._resolvePeer(target);
|
|
808
|
+
if (!entity.certFingerprint)
|
|
809
|
+
throw ownershipFailure('unavailable', 'entity current certificate is unavailable', true);
|
|
810
|
+
if (observed && observed.entity_cert_fingerprint !== entity.certFingerprint)
|
|
811
|
+
throw ownershipFailure('conflict', 'entity authority changed during verification');
|
|
812
|
+
if (meta.entity_cert_fingerprint !== entity.certFingerprint)
|
|
813
|
+
throw ownershipFailure('conflict', 'entity agent.md certificate generation changed');
|
|
814
|
+
return { type: meta.type, owner_aid: owner, entity_cert_fingerprint: entity.certFingerprint };
|
|
815
|
+
}
|
|
816
|
+
_ownerUploadAuthorization(target, signedContent, owner) {
|
|
817
|
+
const bodySha256 = crypto.createHash('sha256').update(signedContent, 'utf-8').digest('hex');
|
|
818
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
819
|
+
const nonce = crypto.randomBytes(16).toString('hex');
|
|
820
|
+
const canonical = `AUN-AGENT-MD-UPLOAD-V1;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${timestamp};${nonce}`;
|
|
821
|
+
const signed = owner.sign(canonical);
|
|
822
|
+
if (!signed.ok || !signed.data) {
|
|
823
|
+
const message = signed.error?.message ?? 'owner authorization signing failed';
|
|
824
|
+
throw new ClientSignatureError(message);
|
|
825
|
+
}
|
|
826
|
+
const payload = {
|
|
827
|
+
version: 1,
|
|
828
|
+
target_aid: target.toLowerCase(),
|
|
829
|
+
uploader_aid: owner.aid.toLowerCase(),
|
|
830
|
+
body_sha256: bodySha256,
|
|
831
|
+
timestamp,
|
|
832
|
+
nonce,
|
|
833
|
+
cert_fingerprint: owner.certFingerprint,
|
|
834
|
+
signature: signed.data.signature,
|
|
835
|
+
};
|
|
836
|
+
return Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
837
|
+
}
|
|
838
|
+
async _uploadHttp(target, signedContent, ownerAuthorization = '', condition) {
|
|
839
|
+
if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
|
|
840
|
+
throw ownershipFailure('persistence_unavailable', 'agent.md upload requires its original document condition');
|
|
308
841
|
const gatewayUrl = await this._resolveGateway(target);
|
|
309
|
-
const
|
|
310
|
-
const
|
|
842
|
+
const authorization = String(ownerAuthorization ?? '').trim();
|
|
843
|
+
const meta = validateAgentMdDocument(signedContent, { expectedAid: target, validateOwnership: false }).frontmatter;
|
|
844
|
+
const entity = ['group', 'org'].includes(String(meta.type));
|
|
311
845
|
const uploadHeaders = {
|
|
312
|
-
|
|
846
|
+
'X-AUN-Signature-Format': 'multi-v1',
|
|
313
847
|
'Content-Type': 'text/markdown; charset=utf-8',
|
|
848
|
+
...(!entity && !legacyRelationship(meta) ? { 'X-AUN-Ownership-Check': '1' } : {}),
|
|
849
|
+
...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }),
|
|
314
850
|
...(this._uploadViaGatewayHost ? { Host: target } : {}),
|
|
315
|
-
...(
|
|
851
|
+
...(authorization && !entity ? { 'X-AUN-AgentMD-Owner-Authorization': authorization } : {}),
|
|
316
852
|
};
|
|
853
|
+
if (!authorization || entity) {
|
|
854
|
+
const token = await this._uploadAccessToken(entity ? this._ownerAid() : target, gatewayUrl);
|
|
855
|
+
uploadHeaders.Authorization = `Bearer ${token}`;
|
|
856
|
+
}
|
|
317
857
|
const uploadRequest = (url) => requestText(url, {
|
|
318
858
|
method: 'PUT',
|
|
319
859
|
verifySsl: this._verifySsl,
|
|
@@ -329,11 +869,12 @@ export class AgentMdManager {
|
|
|
329
869
|
if (fallbackUrl && fallbackUrl !== uploadUrl)
|
|
330
870
|
response = await uploadRequest(fallbackUrl);
|
|
331
871
|
}
|
|
332
|
-
if (response.status === 404)
|
|
872
|
+
if (response.status === 404)
|
|
333
873
|
throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
|
|
334
|
-
}
|
|
335
874
|
if (response.status < 200 || response.status >= 300) {
|
|
336
875
|
const message = response.text.trim();
|
|
876
|
+
if (entity)
|
|
877
|
+
throw ownershipFailure(response.status === 409 || response.status === 412 ? 'conflict' : response.status === 401 || response.status === 403 ? 'forbidden' : response.status === 400 || response.status === 422 ? 'invalid' : response.status === 428 ? 'unsupported_protocol' : 'unavailable', `upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`, response.status >= 500);
|
|
337
878
|
throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
338
879
|
}
|
|
339
880
|
let payload;
|
|
@@ -343,21 +884,9 @@ export class AgentMdManager {
|
|
|
343
884
|
catch (err) {
|
|
344
885
|
throw new AUNError(`upload agent.md returned invalid JSON payload: ${err instanceof Error ? err.message : String(err)}`);
|
|
345
886
|
}
|
|
346
|
-
if (!isJsonObject(payload))
|
|
887
|
+
if (!isJsonObject(payload))
|
|
347
888
|
throw new AUNError('upload agent.md returned invalid JSON payload');
|
|
348
|
-
}
|
|
349
|
-
const result = payload;
|
|
350
|
-
this.saveRecord(target, {
|
|
351
|
-
content: signedContent,
|
|
352
|
-
local_etag: AgentMdManager.contentEtag(signedContent),
|
|
353
|
-
remote_etag: String(result.etag ?? '').trim() || undefined,
|
|
354
|
-
last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(),
|
|
355
|
-
fetched_at: Date.now(),
|
|
356
|
-
checked_at: Date.now(),
|
|
357
|
-
remote_status: String(result.etag ?? '').trim() ? 'found' : 'unknown',
|
|
358
|
-
last_error: '',
|
|
359
|
-
});
|
|
360
|
-
return { ...result };
|
|
889
|
+
return { ...payload };
|
|
361
890
|
}
|
|
362
891
|
async download(aid, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
|
|
363
892
|
const target = this.safeAid(String(aid ?? this._ownerAid() ?? '').trim());
|
|
@@ -576,15 +1105,15 @@ export class AgentMdManager {
|
|
|
576
1105
|
async _resolveGateway(aid) {
|
|
577
1106
|
return String(await this._gatewayResolver?.(aid) ?? '');
|
|
578
1107
|
}
|
|
579
|
-
async _resolvePeer(aid, certFingerprint) {
|
|
580
|
-
const expectedFp =
|
|
1108
|
+
async _resolvePeer(aid, certFingerprint, signedAt) {
|
|
1109
|
+
const expectedFp = certFingerprint ? `sha256:${normalizeFingerprintHex(certFingerprint)}` : '';
|
|
581
1110
|
const current = this._currentAid();
|
|
582
|
-
if (current && current.aid.toLowerCase() === aid.toLowerCase()) {
|
|
583
|
-
if (!expectedFp || certMatchesFingerprint(current.certPem, expectedFp))
|
|
1111
|
+
if (signedAt === undefined && current && current.aid.toLowerCase() === aid.toLowerCase()) {
|
|
1112
|
+
if (!expectedFp || certMatchesFingerprint(current.certPem, expectedFp) || publicKeyMatchesFingerprint(current.certPem, expectedFp))
|
|
584
1113
|
return current;
|
|
585
1114
|
}
|
|
586
|
-
const peer = await this._peerResolver?.(aid, expectedFp || null);
|
|
587
|
-
if (!(peer instanceof AID)) {
|
|
1115
|
+
const peer = await this._peerResolver?.(aid, expectedFp || null, signedAt);
|
|
1116
|
+
if (!(peer instanceof AID) || peer.aid.toLowerCase() !== aid.toLowerCase()) {
|
|
588
1117
|
throw new StateError(`agent.md peer resolver did not return AID for ${aid}`);
|
|
589
1118
|
}
|
|
590
1119
|
return peer;
|
|
@@ -595,13 +1124,20 @@ export class AgentMdManager {
|
|
|
595
1124
|
throw new StateError('authenticate did not return access_token');
|
|
596
1125
|
return token;
|
|
597
1126
|
}
|
|
1127
|
+
async _uploadAccessToken(aid, gatewayUrl) {
|
|
1128
|
+
const resolver = this._uploadAccessTokenResolver ?? this._accessTokenResolver;
|
|
1129
|
+
const token = String(await resolver?.(aid, gatewayUrl) ?? '').trim();
|
|
1130
|
+
if (!token)
|
|
1131
|
+
throw new StateError('authenticate did not return access_token');
|
|
1132
|
+
return token;
|
|
1133
|
+
}
|
|
598
1134
|
async _head(aid) {
|
|
599
1135
|
const target = this.safeAid(aid);
|
|
600
1136
|
const gatewayUrl = await this._resolveGateway(target);
|
|
601
1137
|
const response = await requestText(this._url(target, gatewayUrl), {
|
|
602
1138
|
method: 'HEAD',
|
|
603
1139
|
verifySsl: this._verifySsl,
|
|
604
|
-
headers: { Accept: 'text/markdown' },
|
|
1140
|
+
headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
|
|
605
1141
|
timeoutMs: HEAD_HTTP_TIMEOUT_MS,
|
|
606
1142
|
});
|
|
607
1143
|
if (response.status === 404) {
|
|
@@ -639,7 +1175,7 @@ export class AgentMdManager {
|
|
|
639
1175
|
this._log.debug(`downloadAgentMd enter: aid=${target}`);
|
|
640
1176
|
const gatewayUrl = await this._resolveGateway(target);
|
|
641
1177
|
const cached = this.loadRecord(target) ?? {};
|
|
642
|
-
const headers = { Accept: 'text/markdown' };
|
|
1178
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' };
|
|
643
1179
|
const etag = String(cached.remote_etag ?? cached.etag ?? cached.local_etag ?? '').trim();
|
|
644
1180
|
const lastModified = String(cached.last_modified ?? '').trim();
|
|
645
1181
|
let response = await requestText(this._url(target, gatewayUrl), {
|
|
@@ -647,6 +1183,8 @@ export class AgentMdManager {
|
|
|
647
1183
|
verifySsl: this._verifySsl,
|
|
648
1184
|
headers,
|
|
649
1185
|
timeoutMs,
|
|
1186
|
+
redirectsLeft: 0,
|
|
1187
|
+
strictUtf8: true,
|
|
650
1188
|
});
|
|
651
1189
|
let reusedCachedNotModified = false;
|
|
652
1190
|
if (response.status === 304 && cached.content !== undefined && cached.content !== null) {
|
|
@@ -665,8 +1203,10 @@ export class AgentMdManager {
|
|
|
665
1203
|
response = await requestText(this._url(target, gatewayUrl), {
|
|
666
1204
|
method: 'GET',
|
|
667
1205
|
verifySsl: this._verifySsl,
|
|
668
|
-
headers: { Accept: 'text/markdown' },
|
|
1206
|
+
headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
|
|
669
1207
|
timeoutMs,
|
|
1208
|
+
redirectsLeft: 0,
|
|
1209
|
+
strictUtf8: true,
|
|
670
1210
|
});
|
|
671
1211
|
}
|
|
672
1212
|
if (response.status === 404) {
|
|
@@ -678,10 +1218,24 @@ export class AgentMdManager {
|
|
|
678
1218
|
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
679
1219
|
}
|
|
680
1220
|
const content = response.text;
|
|
681
|
-
const parsed = parseAgentMdTailSignature(content);
|
|
1221
|
+
const parsed = parseAgentMdTailSignature(content, true);
|
|
1222
|
+
let entity = false;
|
|
1223
|
+
let frontmatter = {};
|
|
1224
|
+
try {
|
|
1225
|
+
frontmatter = validateAgentMdReadDocument(content).frontmatter;
|
|
1226
|
+
entity = ['group', 'org'].includes(String(frontmatter.type));
|
|
1227
|
+
}
|
|
1228
|
+
catch { }
|
|
1229
|
+
try {
|
|
1230
|
+
frontmatter = validateAgentMdReadDocument(content, { expectedAid: target, requireSignature: entity }).frontmatter;
|
|
1231
|
+
}
|
|
1232
|
+
catch (error) {
|
|
1233
|
+
throw ownershipFailure('invalid', error instanceof Error ? error.message : String(error));
|
|
1234
|
+
}
|
|
682
1235
|
const expectedFp = parsed.fields?.cert_fingerprint || parsed.fields?.public_key_fingerprint || null;
|
|
683
|
-
const peer = await this._resolvePeer(target, expectedFp);
|
|
684
|
-
const
|
|
1236
|
+
const peer = await this._resolvePeer(entity ? parsed.fields.signer_aid ?? target : target, expectedFp, parsed.fields?.timestamp ? Number(parsed.fields.timestamp) : undefined);
|
|
1237
|
+
const generic = await this._verifyDocument(content, target, peer);
|
|
1238
|
+
const verified = resultOk({ ...generic, aid: target, cert_fingerprint: parsed.fields?.cert_fingerprint, timestamp: Number(parsed.fields?.timestamp) });
|
|
685
1239
|
if (!verified.ok || !verified.data) {
|
|
686
1240
|
const message = verified.error?.message ?? 'agent.md verification failed';
|
|
687
1241
|
throw new AUNError(message);
|
|
@@ -690,9 +1244,31 @@ export class AgentMdManager {
|
|
|
690
1244
|
const statusText = String(signature.status ?? 'invalid');
|
|
691
1245
|
const reason = String(signature.reason ?? '').trim();
|
|
692
1246
|
if (statusText === 'invalid') {
|
|
693
|
-
throw new ClientSignatureError(`agent.md verification failed for ${target}${reason ? `: ${reason}` : ''}
|
|
1247
|
+
throw new ClientSignatureError(`agent.md verification failed for ${target}${reason ? `: ${reason}` : ''}`, entity ? { stringCode: 'invalid' } : {});
|
|
1248
|
+
}
|
|
1249
|
+
let ownershipStatus;
|
|
1250
|
+
if (entity)
|
|
1251
|
+
ownershipStatus = 'not_applicable';
|
|
1252
|
+
else {
|
|
1253
|
+
try {
|
|
1254
|
+
validateOwnershipFrontmatter(frontmatter, target);
|
|
1255
|
+
const rawProof = (frontmatter.ownership_proof ?? {});
|
|
1256
|
+
const subjectSignature = rawProof.subject_signature;
|
|
1257
|
+
const ownerSignature = rawProof.owner_signature;
|
|
1258
|
+
const release = frontmatter.ownership_release;
|
|
1259
|
+
const subjectStatement = subjectSignature?.statement;
|
|
1260
|
+
const subjectCertPem = subjectSignature ? (await this._resolvePeer(target, String(subjectSignature.cert_fingerprint), isJsonObject(subjectStatement) ? Number(subjectStatement.signed_at) : undefined)).certPem : undefined;
|
|
1261
|
+
const ownerAid = String(ownerSignature ? frontmatter.owner : release?.statement && typeof release.statement === 'object' ? release.statement.owner_aid : '');
|
|
1262
|
+
const ownerFingerprint = String(ownerSignature?.cert_fingerprint ?? release?.cert_fingerprint ?? '');
|
|
1263
|
+
const ownerStatement = (ownerSignature ?? release)?.statement;
|
|
1264
|
+
const ownerCertPem = ownerFingerprint ? (await this._resolvePeer(ownerAid, ownerFingerprint, isJsonObject(ownerStatement) ? Number(ownerStatement.signed_at) : undefined)).certPem : undefined;
|
|
1265
|
+
ownershipStatus = verifyOwnershipFrontmatterStatus(frontmatter, subjectCertPem, ownerCertPem);
|
|
1266
|
+
}
|
|
1267
|
+
catch {
|
|
1268
|
+
ownershipStatus = 'invalid';
|
|
1269
|
+
}
|
|
694
1270
|
}
|
|
695
|
-
const verification = { status: statusText };
|
|
1271
|
+
const verification = { status: statusText, ownership_status: ownershipStatus };
|
|
696
1272
|
if (reason)
|
|
697
1273
|
verification.reason = reason;
|
|
698
1274
|
const responseEtag = headerValue(response.headers, 'etag');
|
|
@@ -868,14 +1444,17 @@ export class AgentMdManager {
|
|
|
868
1444
|
payload.aid = this.safeAid(aid);
|
|
869
1445
|
this._atomicWriteText(this.metaPath(aid), `${JSON.stringify(payload, null, 2)}\n`);
|
|
870
1446
|
}
|
|
871
|
-
_readRecordUnlocked(aid) {
|
|
1447
|
+
_readRecordUnlocked(aid, strict = false) {
|
|
872
1448
|
const filePath = this.metaPath(aid);
|
|
873
1449
|
if (!fs.existsSync(filePath))
|
|
874
1450
|
return {};
|
|
875
1451
|
try {
|
|
876
1452
|
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
877
|
-
if (!isJsonObject(parsed))
|
|
1453
|
+
if (!isJsonObject(parsed)) {
|
|
1454
|
+
if (strict)
|
|
1455
|
+
throw ownershipFailure('persistence_unavailable', `agent.md metadata is not a mapping: aid=${aid}`);
|
|
878
1456
|
return {};
|
|
1457
|
+
}
|
|
879
1458
|
const record = {};
|
|
880
1459
|
for (const [key, value] of Object.entries(parsed)) {
|
|
881
1460
|
if (key !== 'content')
|
|
@@ -888,6 +1467,11 @@ export class AgentMdManager {
|
|
|
888
1467
|
return record;
|
|
889
1468
|
}
|
|
890
1469
|
catch (err) {
|
|
1470
|
+
if (strict) {
|
|
1471
|
+
if (err instanceof AUNError && err.stringCode === 'persistence_unavailable')
|
|
1472
|
+
throw err;
|
|
1473
|
+
throw ownershipFailure('persistence_unavailable', `agent.md metadata is damaged: aid=${aid}`);
|
|
1474
|
+
}
|
|
891
1475
|
this._log.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
892
1476
|
return {};
|
|
893
1477
|
}
|