@nelsonlongxiang/dsh-open-a2a-net 0.6.31 → 0.6.32

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 (88) hide show
  1. package/README.md +4 -1
  2. package/README_zh.md +3 -0
  3. package/assets/nexusDist/assets/index-D5H8R8de.js +9 -0
  4. package/assets/nexusDist/assets/index-D9GYZyGZ.css +1 -0
  5. package/assets/nexusDist/index.html +2 -2
  6. package/cli/a2a.mjs +445 -57
  7. package/lib/a2a-client.js +9 -4
  8. package/lib/auth-observer.js +254 -0
  9. package/lib/blob-store.js +210 -0
  10. package/lib/card.js +4 -2
  11. package/lib/cli-deposit.js +122 -7
  12. package/lib/client.js +64 -64
  13. package/lib/client.js.map +1 -1
  14. package/lib/faces/admin.js +133 -43
  15. package/lib/faces/canvas-notify.js +12 -3
  16. package/lib/faces/files.js +155 -0
  17. package/lib/faces/nodes.js +171 -0
  18. package/lib/faces/session-log.js +162 -0
  19. package/lib/faces/wire.js +206 -39
  20. package/lib/index.js +809 -1720
  21. package/lib/joined-store.js +23 -0
  22. package/lib/ledger-conservation.js +97 -0
  23. package/lib/legacy/teams-bridge-inbound.js +156 -0
  24. package/lib/node-bind.js +534 -0
  25. package/lib/node-github.js +72 -0
  26. package/lib/node-self.js +453 -0
  27. package/lib/node-store.js +484 -0
  28. package/lib/node-tasks.js +342 -0
  29. package/lib/origin-claim.js +246 -0
  30. package/lib/peer-directory.js +134 -0
  31. package/lib/receipt.js +26 -1
  32. package/lib/skill/SKILL.md +3 -1
  33. package/lib/skill/references/cli.md +82 -3
  34. package/lib/skill/references/collaboration.md +126 -74
  35. package/lib/skill/references/discovery.md +29 -0
  36. package/lib/skill/references/intake.md +107 -0
  37. package/lib/skill/references/router.md +96 -0
  38. package/lib/skill/references/setup.md +2 -1
  39. package/lib/task-auth.js +483 -0
  40. package/lib/task-ledger.js +324 -12
  41. package/lib/team-store.js +24 -1
  42. package/lib/tools/act.js +383 -0
  43. package/lib/tools/files.js +201 -0
  44. package/lib/tools/info.js +684 -0
  45. package/lib/types/a2a-client.d.ts +10 -4
  46. package/lib/types/auth-observer.d.ts +110 -0
  47. package/lib/types/blob-store.d.ts +83 -0
  48. package/lib/types/cli-deposit.d.ts +38 -0
  49. package/lib/types/faces/admin.d.ts +14 -2
  50. package/lib/types/faces/canvas-notify.d.ts +4 -1
  51. package/lib/types/faces/files.d.ts +53 -0
  52. package/lib/types/faces/nodes.d.ts +69 -0
  53. package/lib/types/faces/session-log.d.ts +44 -0
  54. package/lib/types/faces/wire.d.ts +50 -5
  55. package/lib/types/index.d.ts +27 -0
  56. package/lib/types/joined-store.d.ts +9 -0
  57. package/lib/types/ledger-conservation.d.ts +77 -0
  58. package/lib/types/legacy/teams-bridge-inbound.d.ts +94 -0
  59. package/lib/types/node-bind.d.ts +220 -0
  60. package/lib/types/node-github.d.ts +35 -0
  61. package/lib/types/node-self.d.ts +178 -0
  62. package/lib/types/node-store.d.ts +180 -0
  63. package/lib/types/node-tasks.d.ts +121 -0
  64. package/lib/types/origin-claim.d.ts +119 -0
  65. package/lib/types/peer-directory.d.ts +39 -0
  66. package/lib/types/receipt.d.ts +12 -0
  67. package/lib/types/task-auth.d.ts +271 -0
  68. package/lib/types/task-ledger.d.ts +143 -6
  69. package/lib/types/team-store.d.ts +9 -0
  70. package/lib/types/tools/act.d.ts +52 -0
  71. package/lib/types/tools/files.d.ts +38 -0
  72. package/lib/types/tools/info.d.ts +102 -0
  73. package/lib/types/types.d.ts +6 -0
  74. package/nexus-stage/src/canvas-ops.ts +1 -1
  75. package/nexus-stage/src/federation.ts +1 -0
  76. package/nexus-stage/src/main.ts +41 -18
  77. package/nexus-stage/src/planning-member-handles.ts +34 -0
  78. package/nexus-stage/src/planning-mirror.ts +47 -3
  79. package/nexus-stage/src/planning-reachability.ts +28 -0
  80. package/nexus-stage/src/planning-types.ts +1 -1
  81. package/nexus-stage/src/planning-view.ts +29 -26
  82. package/nexus-stage/src/planning.css +4 -0
  83. package/nexus-stage/src/wb-directory.ts +13 -0
  84. package/nexus-stage/src/workbench.css +3 -0
  85. package/nexus-stage/src/workbench.ts +5 -1
  86. package/package.json +1 -1
  87. package/assets/nexusDist/assets/index-Dz8N80tI.css +0 -1
  88. package/assets/nexusDist/assets/index-jm-cFAhB.js +0 -9
@@ -13,6 +13,8 @@ export type A2aSchedule = (callback: () => void, delayMs: number) => () => void;
13
13
  export type A2aFetch = (url: string, init: {
14
14
  readonly method: 'GET' | 'POST';
15
15
  readonly headers: Record<string, string>;
16
+ /** Optional fetch redirect policy; origin verification sets manual. */
17
+ readonly redirect?: 'follow' | 'error' | 'manual';
16
18
  readonly body?: string;
17
19
  readonly signal?: AbortSignal;
18
20
  }) => Promise<{
@@ -35,11 +37,15 @@ export interface A2aClientOptions {
35
37
  readonly cardCaller: string;
36
38
  /**
37
39
  * gh#9+gh#10 OriginClaim signer (docs/protocol/delivery-origin-auth.md):
38
- * produces the four envelope headers with the NODE'S CARD KEY. Present ⇒
39
- * every card GET and direct POST carries a verifiable origin claim;
40
- * absent ⇒ the caller stays at the named-only posture (transition).
40
+ * produces the envelope headers with the NODE'S CARD KEY, bound to the
41
+ * request's target URL and method. Present ⇒ every card GET and direct
42
+ * POST carries a verifiable origin claim; absent ⇒ the caller stays at
43
+ * the named-only posture (transition).
41
44
  */
42
- readonly originClaim?: () => Record<string, string>;
45
+ readonly originClaim?: (target: {
46
+ readonly url: string;
47
+ readonly method: string;
48
+ }) => Record<string, string>;
43
49
  /** Delayed-callback seam (HTTP timeouts). */
44
50
  readonly schedule: A2aSchedule;
45
51
  /** HTTP seam. */
@@ -0,0 +1,110 @@
1
+ import type { Admission, TaskAuthorization } from './task-auth.ts';
2
+ /** Ring size of both the version history and the affected-task entries. */
3
+ export declare const AUTH_HISTORY_CAP = 32;
4
+ /** Longest refusal text kept on an affected task (the faces' texts fit; a longer one is cut, flagged by `…`). */
5
+ export declare const AFFECTED_REASON_CAP = 400;
6
+ /** One recorded allowlist version and what it hashed. */
7
+ export interface AuthVersionEntry {
8
+ readonly version: string;
9
+ readonly at: number;
10
+ /** Why it was observed: `boot`, `joined`, `canvas`, `roster`, or `lazy` (a read noticed a change no hook reported — CLI expiry, config). */
11
+ readonly cause: string;
12
+ /** The canonical inputs (task-auth.ts canonicalAllowlist), parsed. */
13
+ readonly inputs: unknown;
14
+ }
15
+ /**
16
+ * One task's re-judgement. `basis-changed`: still admitted, but on teams that
17
+ * no longer overlap the record's; `would-revoke`: today's records refuse the
18
+ * caller — `reason` is that refusal text.
19
+ */
20
+ export interface AffectedTask {
21
+ readonly taskId: string;
22
+ readonly direction: 'inbound' | 'outbound';
23
+ readonly caller: string;
24
+ readonly target: string;
25
+ readonly verdict: 'basis-changed' | 'would-revoke';
26
+ readonly reason: string;
27
+ }
28
+ /** One observation that found affected tasks. */
29
+ export interface AffectedEntry {
30
+ readonly at: number;
31
+ /** The previously recorded version; null when there was no history yet. */
32
+ readonly from_version: string | null;
33
+ readonly to_version: string;
34
+ readonly cause: string;
35
+ readonly tasks: readonly AffectedTask[];
36
+ }
37
+ /** The minimum of a ledger row the judgement reads. */
38
+ export interface JudgedRow {
39
+ readonly taskId: string;
40
+ readonly direction?: 'inbound' | 'outbound';
41
+ readonly auth?: TaskAuthorization;
42
+ }
43
+ /**
44
+ * Re-judge rows against today's records — pure: `judge` must be a read-only
45
+ * admission decision (task-auth.ts callerAdmission over the live records).
46
+ * A row is judged when it carries a record and either was born under another
47
+ * version than `version` or `cacheResting(row)` says its admission rests on
48
+ * the peer-card cache (`peer-org`, which the version does not hash; by
49
+ * default: the birth record's `via`).
50
+ * @returns the affected rows, in ledger order; unaffected rows are omitted.
51
+ */
52
+ export declare function judgeAffected(rows: readonly JudgedRow[], version: string, judge: (caller: string, target: string, ingress: 'wire' | 'local') => Admission, cacheResting?: (row: JudgedRow) => boolean): AffectedTask[];
53
+ /** What one {@link AuthObserver.observe} call needs from the host. */
54
+ export interface ObserveInput {
55
+ readonly version: string;
56
+ /** The canonical inputs the version hashed (recorded with a new version). */
57
+ readonly inputs: () => unknown;
58
+ readonly cause: string;
59
+ readonly rows: () => readonly JudgedRow[];
60
+ readonly judge: (caller: string, target: string, ingress: 'wire' | 'local') => Admission;
61
+ }
62
+ /**
63
+ * The persisted observer. One instance per host; every entry point is
64
+ * total and never throws (an unwritable home degrades to memory-only).
65
+ */
66
+ export declare class AuthObserver {
67
+ private readonly path;
68
+ private readonly now;
69
+ private versions;
70
+ private affected;
71
+ /**
72
+ * The last report per task ({@link reportKey}), so a task still affected
73
+ * the same way is not listed again — on a peer-org recheck poll, nor on a
74
+ * later unrelated version move. A task judged unaffected is forgotten, so
75
+ * a relapse reports again; a task gone from the rows is pruned.
76
+ * Boot-local by design: after a restart the next version change reports
77
+ * afresh.
78
+ */
79
+ private readonly reported;
80
+ /**
81
+ * The rule of each task's latest passing judgement. Whether a row rests on
82
+ * the peer-card cache is read from here first, the birth record second: a
83
+ * row born remote-ref-samezone that today passes only through `peer-org`
84
+ * is rechecked like one born there (and stops being once a firm rule
85
+ * admits it again).
86
+ */
87
+ private readonly lastRule;
88
+ constructor(path: string, now?: () => number);
89
+ /** The version history, newest first. */
90
+ history(): readonly AuthVersionEntry[];
91
+ /** The affected-task entries, newest first. */
92
+ entries(): readonly AffectedEntry[];
93
+ /**
94
+ * Record the current version when it moved, and re-judge.
95
+ * - Version moved: append it to the history; judge every row born under
96
+ * another version and every cache-resting row.
97
+ * - Version unchanged: only cache-resting rows are re-judged.
98
+ * - Cause `boot`: cache-resting rows are not judged at all — the peer-card
99
+ * cache is empty at boot, so every one of them would read as refused.
100
+ * The first read after boot (`lazy`) rechecks them.
101
+ * Either way a task is reported only when its report differs from the
102
+ * last one for it ({@link reportKey}).
103
+ * @returns the new affected entry, or undefined when nothing was affected.
104
+ */
105
+ observe(input: ObserveInput): AffectedEntry | undefined;
106
+ /** Forget the per-task memory of tasks no longer among the unsettled rows. */
107
+ private prune;
108
+ private restore;
109
+ private persist;
110
+ }
@@ -0,0 +1,83 @@
1
+ /** One file's ceiling, in bytes (64 MiB; design §8). */
2
+ export declare const BLOB_FILE_MAX: number;
3
+ /** The store's ceiling, in bytes (1 GiB). */
4
+ export declare const BLOB_TOTAL_MAX: number;
5
+ /** The store's ceiling, in blobs. */
6
+ export declare const BLOB_COUNT_MAX = 256;
7
+ /** How long a grant lets its target pull (24h). */
8
+ export declare const BLOB_GRANT_TTL_MS: number;
9
+ /** A sanitized file name's length cap. */
10
+ export declare const BLOB_NAME_MAX = 120;
11
+ /** One grant: `to` may pull `sha256` until `expiresAt`. */
12
+ export interface BlobGrant {
13
+ readonly sha256: string;
14
+ readonly size: number;
15
+ readonly name: string;
16
+ /** The target team or handle, exactly as the sender routed to it. */
17
+ readonly to: string;
18
+ /** The staging session (or CLI handle). */
19
+ readonly by: string;
20
+ readonly grantedAt: number;
21
+ readonly expiresAt: number;
22
+ }
23
+ export type StageResult = {
24
+ readonly ok: true;
25
+ readonly sha256: string;
26
+ readonly size: number;
27
+ readonly name: string;
28
+ readonly expiresAt: number;
29
+ } | {
30
+ readonly ok: false;
31
+ readonly code: 'bad-request' | 'too-large' | 'full';
32
+ readonly error: string;
33
+ };
34
+ /** A file name that can only land inside the inbox: no path, no `..`, no control characters, bounded. */
35
+ export declare function sanitizeBlobName(name: string): string;
36
+ /** The one-line reference a session message carries (design §3.1). */
37
+ export declare function formatFileRef(ref: {
38
+ sha256: string;
39
+ size: number;
40
+ name: string;
41
+ from: string;
42
+ }): string;
43
+ /** Parse a reference line (anywhere in a message); undefined when it is not a well-formed v1 reference. */
44
+ export declare function parseFileRef(text: string): {
45
+ sha256: string;
46
+ size: number;
47
+ name: string;
48
+ from: string;
49
+ } | undefined;
50
+ export declare class BlobStore {
51
+ private readonly root;
52
+ private readonly now;
53
+ private grants;
54
+ /**
55
+ * @param root - the store's directory (`<home>/a2a`); '' = in-memory grants and no blob files (tests of the grant logic only).
56
+ * @param now - clock (tests).
57
+ */
58
+ constructor(root: string, now?: () => number);
59
+ private get grantsFile();
60
+ private blobPath;
61
+ /** Stage bytes for `to`; a repeat of the same (sha256, to) only extends the grant. */
62
+ stage(bytes: Uint8Array, meta: {
63
+ name: string;
64
+ to: string;
65
+ by: string;
66
+ }): StageResult;
67
+ /**
68
+ * The live grant that lets one of `callerTeams` pull `sha256`, if any. `callerTeams` are the
69
+ * teams the WIRE FACE proved for the caller (OriginClaim + membership) — never self-described.
70
+ */
71
+ grantFor(sha256: string, callerTeams: readonly string[]): BlobGrant | undefined;
72
+ /** The bytes of a granted blob (undefined when the file is gone or does not hash back). */
73
+ read(grant: BlobGrant): Uint8Array | undefined;
74
+ /** Live grants (for the state face). */
75
+ list(): readonly BlobGrant[];
76
+ private hasBlob;
77
+ /** Blobs some live grant still names, with their sizes. */
78
+ private liveBlobs;
79
+ /** Drop expired grants and every blob file no live grant names. */
80
+ private sweep;
81
+ private persist;
82
+ private restore;
83
+ }
@@ -29,11 +29,42 @@ export interface CliMail {
29
29
  readonly at: number;
30
30
  readonly from: string;
31
31
  readonly text: string;
32
+ /**
33
+ * gh#113: the entry's place in its handle's mailbox — 1, 2, 3, … in
34
+ * arrival order, never reused, kept across restarts. A reader that keeps
35
+ * the last seq it read sees exactly what is new, and any gap below
36
+ * `oldest_seq` is mail the cap pushed out before it was read.
37
+ */
38
+ readonly seq: number;
39
+ /** gh#113: set when `text` was cut to MAILBOX_TEXT_CAP; `chars` is the original length. */
40
+ readonly truncated?: true;
41
+ readonly chars?: number;
32
42
  }
43
+ /** gh#113: one mailbox read — the rows plus what a reader needs to notice loss. */
44
+ export interface CliMailbox {
45
+ /** Newest first, as before gh#113. */
46
+ readonly mail: readonly CliMail[];
47
+ /** Changes when the store starts empty (no snapshot, or a corrupt one): seq restarted at 1. */
48
+ readonly epoch: string;
49
+ /** The seq the next delivery will get; `next_seq - 1` is the newest ever written. */
50
+ readonly next_seq: number;
51
+ /** The oldest seq still held, or `next_seq` when the mailbox is empty. */
52
+ readonly oldest_seq: number;
53
+ /** Entries the cap pushed out for this handle, all time. */
54
+ readonly dropped: number;
55
+ }
56
+ export declare const MAILBOX_CAP = 64;
57
+ export declare const MAILBOX_TEXT_CAP = 4000;
33
58
  export declare class CliDepositStore {
34
59
  private readonly path;
35
60
  private deposits;
36
61
  private mailbox;
62
+ private nextSeq;
63
+ private dropped;
64
+ private truncated;
65
+ private epoch;
66
+ /** gh#113 review: true while the last write failed — the epoch on disk no longer matches memory. */
67
+ private unpersisted;
37
68
  private now;
38
69
  constructor(path: string, options?: {
39
70
  now?: () => number;
@@ -72,6 +103,13 @@ export declare class CliDepositStore {
72
103
  }>;
73
104
  mail(handle: string, from: string, text: string): void;
74
105
  readMail(handle: string): readonly CliMail[];
106
+ /** gh#113: the mailbox with its seq bounds and loss counter; `afterSeq` keeps only newer rows. */
107
+ readMailbox(handle: string, afterSeq?: number): CliMailbox;
108
+ /** gh#113: fleet-wide loss counters for the state face (the team verifier reads them). */
109
+ mailStats(): {
110
+ dropped: number;
111
+ truncated: number;
112
+ };
75
113
  private sweep;
76
114
  /** Mark one row expired (tombstone) and persist. */
77
115
  private markExpired;
@@ -15,6 +15,7 @@ import type { LayoutStore } from '../layout-store.ts';
15
15
  import type { PeerStore } from '../peer-store.ts';
16
16
  import { type TaskLedger } from '../task-ledger.ts';
17
17
  import type { TeamMembershipStore } from '../team-store.ts';
18
+ import type { OriginClaimTarget } from '../origin-claim.ts';
18
19
  /** Remote-team rows cached for the state face (see refreshRemoteRows). */
19
20
  export type RemoteRowsCache = {
20
21
  at: number;
@@ -25,11 +26,12 @@ export type RemoteRowsCache = {
25
26
  via?: string;
26
27
  workspace?: string;
27
28
  legacy?: boolean;
29
+ unreachable?: boolean;
28
30
  teams?: readonly string[];
29
31
  }[];
30
32
  } | undefined;
31
33
  /** Everything the admin faces used to capture from `apply()`. */
32
- export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim' | 'callerLinkedToThisHost' | 'cachedCardFetch'> {
34
+ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim' | 'callerLinkedToThisHost' | 'cachedCardFetch' | 'peerFetch'> {
33
35
  readonly ctx: Context;
34
36
  readonly config: Config;
35
37
  readonly logger: ReturnType<Context['logger']>;
@@ -51,7 +53,7 @@ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim
51
53
  link: 'linked' | 'stranger';
52
54
  verified: boolean;
53
55
  };
54
- readonly originClaimHeaders: () => Record<string, string>;
56
+ readonly originClaimHeaders: (target: OriginClaimTarget) => Record<string, string>;
55
57
  readonly refreshRemoteRows: () => void;
56
58
  readonly joinedSessions: JoinedSessions;
57
59
  readonly exposureStore: ExposureStore;
@@ -61,6 +63,8 @@ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim
61
63
  readonly canvasStore: CanvasStore;
62
64
  readonly layoutStore: LayoutStore;
63
65
  readonly taskLedger: TaskLedger;
66
+ /** #114 S6: observe (lazily) and return the /state `authorization` block. */
67
+ readonly authorizationView: () => unknown;
64
68
  readonly idempotencyStore: IdempotencyStore;
65
69
  readonly inFlightRoutes: Map<number, InFlightRoute>;
66
70
  readonly recentActivity: RouteActivityEntry[];
@@ -108,6 +112,12 @@ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim
108
112
  readonly id8: (id: string) => string;
109
113
  readonly sessionLabelOf: (agent: Agent) => string;
110
114
  readonly sessionTeamOf: (agent: Agent) => string;
115
+ /** gh#131: the `<zone>/<id8>` handle a canvas may hold for this local session, if it reads as this session. */
116
+ readonly ownCanvasHandleOf: (id: string) => string | undefined;
117
+ /** gh#131: the local session a remote-shaped member ref reads as, if unambiguous. */
118
+ readonly localSessionOfHandle: (ref: string) => string | undefined;
119
+ /** gh#152: the session's canvas teams read through the local lens — a handle-stored membership of this session counts. */
120
+ readonly canvasTeamsOfSession: (id: string) => string[];
111
121
  readonly nodeMetadataOf: (agent: Agent) => {
112
122
  name: string;
113
123
  description: string;
@@ -120,6 +130,8 @@ export interface AdminFaceDeps extends Pick<CanvasNotifyDeps, 'verifyOriginClaim
120
130
  readonly cardCache: Map<string, CardCacheEntry>;
121
131
  readonly remoteRowsCache: RemoteRowsCache;
122
132
  readonly legacyInboundCount: number;
133
+ readonly unboundInboundCount: number;
134
+ readonly boundMissInboundCount: number;
123
135
  readonly ownCardUrl: string;
124
136
  ownerAllowlist: string[];
125
137
  ownerTeamScopeInfo: boolean;
@@ -31,6 +31,7 @@ import type { A2aPeerCard } from '../types.ts';
31
31
  import type { CanvasStore } from '../canvas-store.ts';
32
32
  import type { PeerStore } from '../peer-store.ts';
33
33
  import type { CardCacheEntry } from '../runtime.ts';
34
+ import type { OriginClaimTarget } from '../origin-claim.ts';
34
35
  /** Coalesce a burst of store writes (a drag that adds three members) into one ping. */
35
36
  export declare const CANVAS_NOTIFY_DEBOUNCE_MS = 400;
36
37
  /** A peer's pings refresh its card at most this often. */
@@ -42,7 +43,7 @@ export interface CanvasNotifyDeps {
42
43
  readonly peerStore: PeerStore;
43
44
  readonly canvasStore: CanvasStore;
44
45
  readonly cardCache: Map<string, CardCacheEntry>;
45
- readonly originClaimHeaders: () => Record<string, string>;
46
+ readonly originClaimHeaders: (target: OriginClaimTarget) => Record<string, string>;
46
47
  readonly verifyOriginClaim: (req: IncomingMessage) => Promise<{
47
48
  ok: true;
48
49
  caller: string;
@@ -54,6 +55,8 @@ export interface CanvasNotifyDeps {
54
55
  }>;
55
56
  readonly callerLinkedToThisHost: (caller: string, callerUrl: string, callerCard: A2aPeerCard | undefined) => boolean;
56
57
  readonly cachedCardFetch: (url: string) => Promise<A2aPeerCard | undefined>;
58
+ /** gh#193: the host's guarded peer fetch (refuses while isolated). */
59
+ readonly peerFetch: typeof globalThis.fetch;
57
60
  }
58
61
  /**
59
62
  * Re-read one peer's card now. Each pass pre-stales the entry (the literal
@@ -0,0 +1,53 @@
1
+ /**
2
+ * #91 file channel, slice F1b (docs/design/file-channel.md §3.2, §3.4, §5): the two HTTP faces
3
+ * over the sender host's {@link BlobStore}.
4
+ *
5
+ * - `GET /a2a/blob?sha256=<hex>&caller_session=<label>` — the PULL face. Admission is the same
6
+ * reading the delivery face gives `已验证来源`: an OriginClaim that verifies, and for a host the
7
+ * audience-bound (v2) form signed from a ROOT card URL whose zone covers `caller_session`; a
8
+ * deposited CLI identity proves its own handle. The caller must then match a live grant's `to`:
9
+ * the caller's own label, the signer host itself or one of its canvas teams, or a canvas team of
10
+ * THIS host the caller itself is a member of ({@link grantAdmits}).
11
+ * A refusal never says whether the blob exists. Pulls are rate-limited per proven signer.
12
+ * - `POST /__dsh_a2a/blob?to=<team>&name=<file>` — the control-gated STAGE face (CLI `file send`,
13
+ * design §3.4): raw bytes in, a grant out. No peer can write into the store.
14
+ * @module @nelsonlongxiang/dsh-open-a2a-net/faces/files
15
+ */
16
+ import type { IncomingMessage, ServerResponse } from 'node:http';
17
+ import type { Context } from '@deepseek-ai/cordis';
18
+ import type WebServer from '@deepseek-ai/dsh-host-webserver';
19
+ import type { Config } from '../index.ts';
20
+ import { type BlobStore } from '../blob-store.ts';
21
+ import type { CanvasStore } from '../canvas-store.ts';
22
+ import type { OriginClaimResult } from './wire.ts';
23
+ /** Pulls per proven signer (host zone or CLI handle) in a sliding minute (design §8; the FloodGuard figure). */
24
+ export declare const BLOB_PULLS_PER_MINUTE = 16;
25
+ export interface FileFaceDeps {
26
+ readonly ctx: Context;
27
+ readonly config: Pick<Config, 'apiKey' | 'team'>;
28
+ readonly blobStore: BlobStore;
29
+ readonly canvasStore: Pick<CanvasStore, 'membersOf'>;
30
+ readonly verifyOriginClaim: (req: IncomingMessage, options?: {
31
+ readonly cli?: boolean;
32
+ }) => Promise<OriginClaimResult>;
33
+ readonly secretEqual: (a: string, b: string) => boolean;
34
+ readonly controlRoute: (handler: (req: IncomingMessage, res: ServerResponse) => void) => (req: IncomingMessage, res: ServerResponse) => void;
35
+ readonly recordActivity: (dir: 'in' | 'out', team: string, peer: string, ok: boolean) => void;
36
+ readonly now?: () => number;
37
+ }
38
+ /**
39
+ * Whether a grant's target lets this proven caller pull (design §3.2). For a HOST signer: the
40
+ * caller's own label; the signer host itself (a grant to the whole zone); one of the signer host's
41
+ * canvas teams (`<signer>/canvas/<name>` — that host answers for its own membership); or a canvas
42
+ * team of THIS host (`<zone>/canvas/<name>`; another host's same-named team is a different team) of
43
+ * which the caller ITSELF is a member. A grant to one session is not another session's, even on the
44
+ * same host (#91 F2: the honest host's tool names the pulling session, so a mistake cannot cross
45
+ * sessions). A CLI handle is an identity, never a zone (review of PR #208 R1): only its own grant.
46
+ * `caller` is already proven by `signer`.
47
+ */
48
+ export declare function grantAdmits(to: string, caller: string, signer: string, zone: string, localCanvasMembers: (name: string) => readonly string[], cli?: boolean): boolean;
49
+ /**
50
+ * Register the pull face and the stage face on the shared web server.
51
+ * @param d - the captures listed on {@link FileFaceDeps}.
52
+ */
53
+ export declare function registerFileFaces(webServer: WebServer, d: FileFaceDeps): void;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The `/__dsh_a2a/nodes` face (docs/design/node-identity.md, slice N1).
3
+ *
4
+ * - GET rides the ordinary control trust (loopback or apiKey): the roster,
5
+ * and with `?name=` one node plus its version numbers and current layers.
6
+ * - POST is an IDENTITY write (create / update / archive / write-core /
7
+ * write-mutable / rollback, bind / unbind a session — N2+N3, and track /
8
+ * untrack a task pointer — N6, and approve / reject a queued remote
9
+ * self-update — N4). §8-2 (owner ruling 2026-11-02): it requires
10
+ * the host's apiKey in `X-API-Key` even on loopback — a host with no
11
+ * apiKey refuses identity writes outright. No model tool reaches it.
12
+ * Same-host shell access can still edit the files; the design says so
13
+ * (§7) instead of pretending otherwise.
14
+ * @module src/faces/nodes
15
+ */
16
+ import type { IncomingMessage, ServerResponse } from 'node:http';
17
+ import type { Context } from '@deepseek-ai/cordis';
18
+ import type WebServer from '@deepseek-ai/dsh-host-webserver';
19
+ import type { NodeStore } from '../node-store.ts';
20
+ import type { NodeBinder, NodeBindings } from '../node-bind.ts';
21
+ import type { NodeTasks } from '../node-tasks.ts';
22
+ import type { NodeReviewQueue } from '../node-self.ts';
23
+ /** Identity bodies carry layers of up to 3000 CJK characters; the 10 KiB control default is too small. */
24
+ export declare const NODE_BODY_MAX_BYTES = 32000;
25
+ export interface NodeFaceDeps {
26
+ readonly ctx: Context;
27
+ readonly logger: {
28
+ warn: (format: string, ...args: unknown[]) => void;
29
+ };
30
+ readonly nodeStore: NodeStore;
31
+ readonly bindings: NodeBindings;
32
+ readonly binder: NodeBinder;
33
+ readonly tasks: NodeTasks;
34
+ readonly reviews: NodeReviewQueue;
35
+ readonly apiKey: () => string;
36
+ readonly secretEqual: (a: string, b: string) => boolean;
37
+ readonly controlRoute: (handler: (req: IncomingMessage, res: ServerResponse) => void) => (req: IncomingMessage, res: ServerResponse) => void;
38
+ readonly readJsonBody: (req: IncomingMessage, res: ServerResponse, use: (body: {
39
+ readonly id?: unknown;
40
+ readonly name?: unknown;
41
+ readonly action?: unknown;
42
+ }) => void, maxBytes?: number) => void;
43
+ }
44
+ /** §8-2: identity writes need the configured apiKey in the header — loopback is not enough, and no key means no identity writes. */
45
+ export declare function identityVerdict(apiKey: string, presented: unknown, secretEqual: (a: string, b: string) => boolean): {
46
+ ok: true;
47
+ } | {
48
+ ok: false;
49
+ status: 401 | 403;
50
+ error: string;
51
+ };
52
+ /** One POST body → status + payload. Pure: the route below only does I/O and the identity gate. */
53
+ export declare function nodeAction(store: NodeStore, binder: NodeBinder, tasks: NodeTasks, reviews: NodeReviewQueue, raw: unknown): {
54
+ status: number;
55
+ payload: Record<string, unknown>;
56
+ };
57
+ /**
58
+ * GET view: the roster with the bindings (and what each live session froze at mount), or one node
59
+ * with its version numbers and current layers. `current` only shows a version that passes the shape
60
+ * and sha checks (`null` otherwise) — the same reading a mount uses.
61
+ */
62
+ export declare function nodeView(store: NodeStore, bindings: NodeBindings, binder: NodeBinder, tasks: NodeTasks, reviews: NodeReviewQueue, name: string | null, assembled?: ReadonlyMap<string, {
63
+ core: boolean;
64
+ mutable: boolean;
65
+ }>): {
66
+ status: number;
67
+ payload: Record<string, unknown>;
68
+ };
69
+ export declare function registerNodeFaces(webServer: WebServer, d: NodeFaceDeps): void;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * gh#77 (src/index.ts line ratchet): the two session-log faces, moved verbatim out of apply() —
3
+ * the LOCAL transcript reader `/__dsh_a2a/session-log` and the REMOTE proxy
4
+ * `/__dsh_a2a/remote-session-log`. Behavior unchanged; apply() hands over the captures below.
5
+ * @module @nelsonlongxiang/dsh-open-a2a-net/faces/session-log
6
+ */
7
+ import type { IncomingMessage, ServerResponse } from 'node:http';
8
+ import type { Context } from '@deepseek-ai/cordis';
9
+ import type WebServer from '@deepseek-ai/dsh-host-webserver';
10
+ import type { Config } from '../index.ts';
11
+ import type { PeerStore } from '../peer-store.ts';
12
+ import type { A2aPeerCard } from '../types.ts';
13
+ /** Everything the session-log faces capture from apply(). */
14
+ export interface SessionLogFaceDeps {
15
+ readonly ctx: Context;
16
+ readonly config: Pick<Config, 'team'>;
17
+ readonly controlRoute: (handler: (req: IncomingMessage, res: ServerResponse) => void) => (req: IncomingMessage, res: ServerResponse) => void;
18
+ readonly scanSessionLogList: () => Array<{
19
+ id: string;
20
+ mtimeMs: number;
21
+ sizeBytes: number;
22
+ }>;
23
+ readonly readSessionLogTail: (id: string, limit: number) => {
24
+ ok: true;
25
+ id: string;
26
+ messages: Array<{
27
+ role: string;
28
+ text: string;
29
+ time?: number;
30
+ }>;
31
+ } | {
32
+ ok: false;
33
+ error: string;
34
+ };
35
+ readonly peerStore: Pick<PeerStore, 'list'>;
36
+ readonly peerFetch: typeof globalThis.fetch;
37
+ readonly directoryPeerCandidates: (fetch: (url: string) => Promise<A2aPeerCard | undefined>, team: string, failures: string[]) => Promise<string[]>;
38
+ readonly memoizedCardFetch: () => (url: string) => Promise<A2aPeerCard | undefined>;
39
+ }
40
+ /**
41
+ * Register the local transcript reader and the remote-log proxy on the shared web server.
42
+ * @param d - the captures listed on {@link SessionLogFaceDeps}.
43
+ */
44
+ export declare function registerSessionLogFaces(webServer: WebServer, d: SessionLogFaceDeps): void;
@@ -20,7 +20,16 @@ import type { CliDepositStore } from '../cli-deposit.ts';
20
20
  import type { IdempotencyStore } from '../idempotency-store.ts';
21
21
  import type { JoinedSessions } from '../joined-store.ts';
22
22
  import { type TaskLedger } from '../task-ledger.ts';
23
+ import { type Admission, type SettlementRefusal, type Settler } from '../task-auth.ts';
23
24
  import type { Config } from '../index.ts';
25
+ /**
26
+ * #139 (design §4.4): add `settlement: {status:'refused', reason, task_id}`
27
+ * to whatever JSON object this response ends with. The delivery face has
28
+ * many exits (mailbox, no-wait, waiting, refusals); deferring the head until
29
+ * the body is known keeps one rule for all of them. A non-JSON or non-object
30
+ * body is sent unchanged.
31
+ */
32
+ export declare function withSettlementField(res: ServerResponse, settlement: SettlementRefusal): void;
24
33
  /** The cold-wake answer (mirrors index.ts's local ColdWake). */
25
34
  export type ColdWake = {
26
35
  readonly kind: 'flight';
@@ -98,6 +107,8 @@ export type OriginClaimResult = {
98
107
  caller: string;
99
108
  callerUrl: string;
100
109
  card: A2aPeerCard;
110
+ bound: boolean;
111
+ cli?: true;
101
112
  } | {
102
113
  ok: false;
103
114
  reason: string;
@@ -112,12 +123,22 @@ export interface WireDeps {
112
123
  readonly sessionNodes: Map<string, Agent>;
113
124
  readonly joinedSessions: JoinedSessions;
114
125
  readonly idempotencyStore: IdempotencyStore;
115
- readonly verifyOriginClaim: (req: IncomingMessage) => Promise<OriginClaimResult>;
126
+ readonly verifyOriginClaim: (req: IncomingMessage, options?: {
127
+ readonly cli?: boolean;
128
+ }) => Promise<OriginClaimResult>;
116
129
  readonly callerLinkedToThisHost: (caller: string, callerUrl: string, callerCard: A2aPeerCard | undefined) => boolean;
130
+ /** This host's own card URL (read when a request lands; set once the web server settles). */
131
+ readonly ownCardUrl: string;
117
132
  readonly recordActivity: (dir: 'in' | 'out', team: string, peer: string, ok: boolean) => void;
118
133
  readonly bumpLegacyInbound: () => void;
119
134
  readonly secretEqual: (a: string, b: string) => boolean;
120
135
  readonly a2aCallerRefusal: (caller: string, targetTeam?: string) => string | undefined;
136
+ /** #114 S2: the same decision with the rule and basis that admitted the caller. */
137
+ readonly a2aCallerAdmission: (caller: string, targetTeam?: string) => Admission;
138
+ /** #114 S2: this host's allowlist version right now (design §2.3). */
139
+ readonly allowlistVersionNow: () => string;
140
+ /** #114 S6: record the version if it moved and re-judge affected tasks (observe only). */
141
+ readonly observeAuthorization: (cause: string) => void;
121
142
  readonly scanSessionLogList: () => Array<{
122
143
  id: string;
123
144
  mtimeMs: number;
@@ -137,7 +158,8 @@ export interface WireDeps {
137
158
  };
138
159
  readonly taskLedger: TaskLedger;
139
160
  readonly refuseIfFlooded: (caller: string, message: string, team: string, taskId: string, res: ServerResponse) => boolean;
140
- readonly settleAndAnnounce: (message: string) => void;
161
+ /** Settles a receipt; returns the enforce refusal of the row its header names (#139), if any. */
162
+ readonly settleAndAnnounce: (message: string, settler?: Settler) => SettlementRefusal | undefined;
141
163
  readonly touchWakeDemand: () => void;
142
164
  readonly session: string;
143
165
  readonly resolveAgentForTeam: (team: string) => Agent | undefined;
@@ -145,9 +167,16 @@ export interface WireDeps {
145
167
  readonly canvasLiveAgent: (name: string | undefined, skipId?: string) => Agent | undefined;
146
168
  readonly wakeColdTeam: (team: string, skipId?: string) => ColdWake;
147
169
  readonly probeConsumption: (agent: Agent, taskId: string) => boolean;
170
+ /** A session's own node team (`<zone>/<id8>`), the handle a member is known by. */
171
+ readonly sessionTeamOf: (agent: Agent) => string;
172
+ /** gh#131: the local session one of this host's own `<zone>/<id8>` handles names, if exactly one. */
173
+ readonly localSessionOfHandle: (ref: string) => string | undefined;
148
174
  readonly steerRelay: (agent: Agent, text: string, sender?: string) => void;
149
175
  readonly fromClause: (from: string) => string;
150
- readonly armReceiptAutosend: (target: Agent, taskId: string, callerTeam: string | undefined, deliveredText: string) => void;
176
+ readonly sanitizeFromClause: (from: string) => string;
177
+ readonly armReceiptAutosend: (target: Agent, taskId: string, callerTeam: string | undefined, deliveredText: string, steerFloor: number) => void;
178
+ /** #114 S5: the target's session-log length, read just before a steer. */
179
+ readonly eventCountOf: (agent: Agent) => number;
151
180
  readonly receiptExpectationOf: (receiptTarget: string) => 'receivable' | 'process-label';
152
181
  readonly armAsyncNudge: (agent: Agent, team: string, taskId: string, delayMs: number) => void;
153
182
  readonly liveAgent: () => Agent | undefined;
@@ -159,9 +188,25 @@ export interface WireDeps {
159
188
  readonly nativeTeamsPrepare: (team: string) => Promise<NativePrepared>;
160
189
  readonly nativeTeamsRound: (prepared: Extract<NativePrepared, {
161
190
  ok: true;
162
- }>, team: string, caller: string, message: string, taskId: string | undefined) => Promise<NativeRoundOutcome>;
163
- readonly routeIntoAgent: (team: string, message: string, caller: string, taskId?: string) => Promise<RouteOutcome>;
191
+ }>, team: string, caller: string, message: string, taskId: string | undefined, proofClause: string, external?: AbortSignal, proofHeader?: (team: string, taskId: string, from: string, proofClause: string) => string) => Promise<NativeRoundOutcome>;
192
+ readonly routeIntoAgent: (team: string, message: string, caller: string, taskId?: string, proofClause?: string, directHeader?: (from: string) => string) => Promise<RouteOutcome>;
164
193
  }
194
+ /**
195
+ * #187: what a verified claim may vouch for at settlement. The signing key always (the card was
196
+ * fetched and its key verified); the ORIGIN only when the claim URL is that origin's root — a card
197
+ * fetched from a path under an origin proves the path's publisher, not the origin's host.
198
+ */
199
+ export declare function settlerClaimFields(claim: {
200
+ ok: boolean;
201
+ cli?: true;
202
+ callerUrl?: string;
203
+ card?: {
204
+ publicKey: string;
205
+ };
206
+ }, originOf: (url: string) => string): {
207
+ claimOrigin?: string;
208
+ claimKey?: string;
209
+ };
165
210
  /**
166
211
  * Register the small wire faces on a settled web server.
167
212
  * @param webServer - the settled host web server.