@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
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Node self-update (docs/design/node-identity.md §6; slice N4): the
3
+ * `node_update_self` tool a session carrying a node may call to write ONE
4
+ * section of its node's mutable layer, and the human review queue.
5
+ *
6
+ * - Only bound sessions get the tool (it is registered with the node's
7
+ * sections at mount). Its parameters cannot name, role, team or touch the
8
+ * core layer: one of the four mutable sections, text, reason, CAS sha.
9
+ * - Session-level taint (§6, H2; owner ruling 2026-11-03 「会话级污染」, which
10
+ * reads §9's `via != host-face` as "this session has received input from
11
+ * outside this host"): once a session has, every self-update it makes is
12
+ * parked in the review queue, where a human approves or rejects it through
13
+ * the apiKey-gated nodes face. Either of two signals taints:
14
+ * 1. the host marks the session when it DELIVERS remote content into it
15
+ * (every relay steer and the queue-mode nudge — {@link SessionTaint});
16
+ * the mark is persisted and never cleared;
17
+ * 2. a scan of the session's whole event log (fail-closed backstop): an
18
+ * `a2a-relay` message that is not a host-built node reminder, a `user`
19
+ * message whose controller rpcId starts with `a2a-` (this plugin's
20
+ * nudge), or any source kind not known here. Context the agent loop
21
+ * injects (runtime/time/skill/tool-job notices…) is neutral.
22
+ * 3. this plugin's own A2A tools that hand the model peer-originated text
23
+ * (any route answer — a reply or an error —, inbound task briefs, peer
24
+ * card rows — {@link toolTaint}) mark the calling session when they return.
25
+ * Messages that carry ANOTHER session's text (a subagent's closing message,
26
+ * a peer agent's message, a referenced transcript) take that session's
27
+ * taint, so a subagent that talked to a peer cannot launder its reply.
28
+ * Only remote EVIDENCE is persisted (a relay, a nudge, a tainted other
29
+ * session, an A2A tool read); an unknown source, an unknown sender or a log
30
+ * with no person's input refuses this one call (fail-closed) without
31
+ * marking, so a host feature this module does not know yet never taints a
32
+ * session forever.
33
+ * The boundary, stated honestly: remote text the model reads through
34
+ * general tools (a fetched page, an issue body) is not a delivery and does
35
+ * not taint; nor can a `kind:'user'` prompt another plugin submits through
36
+ * the session controller be told apart from a person (only this plugin's
37
+ * nudge, rpcId `a2a-*`, is recognised).
38
+ * - Daily cap: at most {@link SELF_UPDATE_DAILY_MAX} node writes per node
39
+ * per UTC day; beyond that the call is refused (not queued).
40
+ * - Each write freezes a full mutable version through the store (quota,
41
+ * CAS, max+1), attributed `by: node:<session>`, `via: node-tool`; the
42
+ * session's frozen prompt does not change — the write is echoed in a live
43
+ * runtime context instead (§5).
44
+ * @module @nelsonlongxiang/dsh-open-a2a-net/node-self
45
+ */
46
+ import { type NodeResult, type NodeStore, type PromptVersion } from './node-store.ts';
47
+ export declare const SELF_UPDATE_DAILY_MAX = 8;
48
+ /** Queued writes awaiting a human, across all nodes. */
49
+ export declare const REVIEW_QUEUE_MAX = 64;
50
+ /** Queued writes per node: a tainted session cannot crowd out every other node's items. */
51
+ export declare const REVIEW_PER_NODE_MAX = 8;
52
+ /** What the log scan may ask about OTHER sessions. */
53
+ export interface SessionLookups {
54
+ /** Whether that session is marked tainted. */
55
+ readonly tainted: (session: string) => boolean;
56
+ /** Whether that id is a session of this host the plugin knows (a live or joined root). */
57
+ readonly local: (session: string) => boolean;
58
+ }
59
+ /** Host-only marker routeTrunk puts on a route result it built from this host alone (a symbol: no peer JSON can carry it). */
60
+ export declare const LOCAL_ROUTE: unique symbol;
61
+ /** Mark a route result as built on this host alone (host refusals, host-built delivery notes, a clean local session's reply). */
62
+ export declare function localRoute<T extends object>(value: T): T;
63
+ /**
64
+ * gh#198 (owner ruling 2026-11-03 「按建议判据做」): whether one of this plugin's A2A tools just handed the
65
+ * model content that came from outside this host — a route that reached (or tried) a peer, inbound task
66
+ * rows, a directory row that is not this host's, a status/probe that names peers. A result built from
67
+ * this host's own facts alone is not remote. `a2a_route` reads the host-written `peer_content` flag the
68
+ * tool sets last (so a peer reply cannot set it); anything else defaults to remote.
69
+ */
70
+ export declare function peerContentOf(name: string, value: unknown): boolean;
71
+ /**
72
+ * The via to mark the calling session with after one of the A2A tools returned, or '' when its result
73
+ * carries nothing from outside this host ({@link peerContentOf}).
74
+ */
75
+ export declare function toolTaint(name: string, value: unknown): string;
76
+ /** The `tool/result` meta the A2A tools persist, so the log backstop reads the same verdict ({@link sessionSource}). */
77
+ export declare const peerContentMeta: (name: string) => (_args: unknown, value: unknown) => {
78
+ a2aPeerContent: boolean;
79
+ };
80
+ /**
81
+ * The event-log backstop of the session taint: whether ANY input in the session's log came from
82
+ * outside this host. A node reminder is local only when the HOST built it (`nodeReminder: true` on an
83
+ * `a2a-relay` source — remote deliveries are relayed by this host with a fixed source shape, so a
84
+ * remote caller cannot set that field). A session with no person's input at all is not local either.
85
+ */
86
+ export declare function sessionSource(events: readonly unknown[], others?: SessionLookups): {
87
+ local: true;
88
+ } | {
89
+ local: false;
90
+ via: string;
91
+ evidence: boolean;
92
+ };
93
+ /** Sessions remembered as tainted; the oldest beyond this are forgotten (a forgotten one re-taints from its log or next delivery). */
94
+ export declare const TAINT_CAP = 4096;
95
+ /**
96
+ * The persisted half of the session taint (`node-taint.json`): the host marks a session the moment it
97
+ * delivers remote content into it, and the mark is never cleared. A file that does not parse is kept
98
+ * in place and read-only, and then EVERY session reads as tainted (fail-closed).
99
+ */
100
+ export declare class SessionTaint {
101
+ private readonly root;
102
+ private readonly now;
103
+ private rows;
104
+ private readOnlyReason;
105
+ constructor(root: string, now?: () => string);
106
+ private get file();
107
+ private restore;
108
+ get readOnly(): string;
109
+ /** Why a session is tainted ('' = it is not). Fail-closed while the file is unreadable. */
110
+ of(session: string): string;
111
+ /** Mark a session (idempotent). Never throws; a mark that cannot be written still holds in memory. */
112
+ mark(session: string, via: string): void;
113
+ }
114
+ export interface ReviewItem {
115
+ readonly id: string;
116
+ readonly node: string;
117
+ readonly section: string;
118
+ readonly text: string;
119
+ readonly reason: string;
120
+ readonly expected_sha256: string;
121
+ readonly session: string;
122
+ /** Why it was parked: the remote source kind of the turn. */
123
+ readonly via: string;
124
+ readonly at: string;
125
+ }
126
+ /** The human review queue for remote-turn self-updates (`node-review.json`; corrupt = kept in place, read-only). */
127
+ export declare class NodeReviewQueue {
128
+ private readonly root;
129
+ private readonly now;
130
+ private items;
131
+ private readOnlyReason;
132
+ constructor(root: string, now?: () => string);
133
+ private get file();
134
+ private restore;
135
+ private commit;
136
+ get readOnly(): string;
137
+ list(node?: string): readonly ReviewItem[];
138
+ get(id: string): ReviewItem | undefined;
139
+ /**
140
+ * Queue one write. A newer proposal for the same node, section and session REPLACES the older one
141
+ * (the human reviews the latest wish, not a backlog of drafts); the text must fit the section quota;
142
+ * each node holds at most {@link REVIEW_PER_NODE_MAX} items and the queue {@link REVIEW_QUEUE_MAX}.
143
+ */
144
+ add(input: Omit<ReviewItem, 'id' | 'at'>): NodeResult<ReviewItem>;
145
+ remove(id: string): NodeResult<true>;
146
+ /** A node archived: its pending items go with it. */
147
+ dropNode(node: string): void;
148
+ }
149
+ /** Node writes (`by: node:…`) frozen for `node` on the UTC day of `now`. */
150
+ export declare function nodeWritesToday(store: NodeStore, node: string, now: string): number;
151
+ export interface SelfUpdateDeps {
152
+ readonly store: NodeStore;
153
+ readonly queue: NodeReviewQueue;
154
+ /** The persisted session taint (marked at remote deliveries). */
155
+ readonly taint: SessionTaint;
156
+ /** The calling session's event log (the taint's fail-closed backstop). */
157
+ readonly events: (agent: unknown) => readonly unknown[];
158
+ /** Whether an id is a session of this host the plugin knows (for messages naming another session). */
159
+ readonly localSession?: (session: string) => boolean;
160
+ readonly now?: () => string;
161
+ /** A write went through (the binder echoes it in the session's live context). */
162
+ readonly onWritten: (session: string, version: PromptVersion, section: string) => void;
163
+ }
164
+ export type SelfUpdateResult = {
165
+ ok: true;
166
+ version: number;
167
+ sha256: string;
168
+ } | {
169
+ ok: false;
170
+ error: string;
171
+ queued?: string;
172
+ code?: string;
173
+ current_sha256?: string;
174
+ };
175
+ /** One self-update call, decided. Pure over its deps (the tool below only adapts it). */
176
+ export declare function selfUpdate(d: SelfUpdateDeps, node: string, session: string, agent: unknown, args: Record<string, unknown>): SelfUpdateResult;
177
+ /** The tool registered on a bound session's own scope. */
178
+ export declare function selfUpdateTool(d: SelfUpdateDeps, node: string, session: string): import("@deepseek-ai/dsh-tools").ToolDefinition;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Node identity store (docs/design/node-identity.md, slice N1): long-lived,
3
+ * human-named nodes that a session can carry, with layered prompt versions.
4
+ *
5
+ * - `nodes.json` under the a2a home holds the node records and the retired
6
+ * names (a name is never reused, and never renamed — §8-1).
7
+ * - Prompt layers live in two namespaces, append-only:
8
+ * `nodes/<name>/core-v<n>.json` (the immutable layer, humans only) and
9
+ * `nodes/<name>/mutable-v<n>.json` (the four-section mutable layer).
10
+ * Every version is a FULL snapshot; the next number is max+1 over the
11
+ * files on disk, so a write after a rollback never collides; a write must
12
+ * name the sha it expects to replace (CAS — parallel sessions of one node).
13
+ * - Identity writes (create / update / archive / core / rollback) are
14
+ * gated by the caller (the nodes face requires apiKey even on loopback,
15
+ * §8-2); this module only validates shapes and keeps the chain closed.
16
+ * - Never-reuse survives a lost roster: create makes the node's version
17
+ * directory at once, and a name with a directory counts as used. A roster
18
+ * that does not parse, or parses into the wrong shape, is copied to
19
+ * `nodes.json.corrupt-<ts>` and LEFT IN PLACE; the store is READ-ONLY for
20
+ * as long as it stays unreadable (every restart), so it is never silently
21
+ * overwritten. Entries dropped as malformed are backed up first
22
+ * (`nodes.json.dropped-<ts>`). Writes go to disk first (tmp + rename) and
23
+ * only then into memory.
24
+ * @module @nelsonlongxiang/dsh-open-a2a-net/node-store
25
+ */
26
+ /** §8-1: node names. */
27
+ export declare const NODE_NAME: RegExp;
28
+ /** Upper bound on live nodes per host (a roster, not a database). */
29
+ export declare const NODE_CAP = 64;
30
+ /** The four mutable sections, in prompt order (§5). */
31
+ export declare const MUTABLE_SECTIONS: readonly ["当前状态", "执行要点", "已知陷阱", "当前重点"];
32
+ export type MutableSection = typeof MUTABLE_SECTIONS[number];
33
+ /** §8-9 quotas, in characters (code points, not UTF-16 units). */
34
+ export declare const QUOTA: {
35
+ readonly core: 2000;
36
+ readonly mutable: 3000;
37
+ readonly mutableSection: 1000;
38
+ };
39
+ export type Layer = 'core' | 'mutable';
40
+ export interface NodeCarrier {
41
+ /** Checkout the node's sessions run in (session controller create). */
42
+ readonly cwd: string;
43
+ /** Agent preset id; '' = the host default. */
44
+ readonly agentPreset: string;
45
+ /** Model selection; '' = the preset's default. */
46
+ readonly model: string;
47
+ }
48
+ export interface NodeRecord {
49
+ readonly name: string;
50
+ readonly label: string;
51
+ readonly role: readonly string[];
52
+ readonly teams: readonly string[];
53
+ readonly carrier: NodeCarrier;
54
+ readonly prompt: {
55
+ readonly core_version: number;
56
+ readonly core_sha256: string;
57
+ readonly mutable_version: number;
58
+ readonly mutable_sha256: string;
59
+ };
60
+ readonly created_by: string;
61
+ readonly created_at: string;
62
+ }
63
+ /** One frozen prompt version (either namespace). */
64
+ export interface PromptVersion {
65
+ readonly layer: Layer;
66
+ readonly version: number;
67
+ /** sha256 over the canonical JSON of {version, sections}. */
68
+ readonly sha256: string;
69
+ readonly parent_version: number;
70
+ /** core: {'core': text}; mutable: the four sections. Always the full layer. */
71
+ readonly sections: Readonly<Record<string, string>>;
72
+ readonly changed: readonly string[];
73
+ readonly reason: string;
74
+ readonly by: string;
75
+ readonly via: string;
76
+ readonly at: string;
77
+ readonly rollback_of?: number;
78
+ }
79
+ export type NodeErrorCode = 'bad-request' | 'not-found' | 'conflict' | 'quota' | 'cap' | 'unavailable';
80
+ export type NodeResult<T> = {
81
+ ok: true;
82
+ value: T;
83
+ } | {
84
+ ok: false;
85
+ error: string;
86
+ code: NodeErrorCode;
87
+ };
88
+ /** The sha a version is pinned by: canonical JSON of {version, sections} (keys sorted). */
89
+ export declare function promptSha(version: number, sections: Readonly<Record<string, string>>): string;
90
+ /**
91
+ * Node store. One writer (the nodes face); read fresh by the state face.
92
+ */
93
+ export declare class NodeStore {
94
+ private readonly now;
95
+ private nodes;
96
+ private retired;
97
+ private readonly root;
98
+ /** Set when the roster on disk could not be read: identity writes are refused until a human repairs it. */
99
+ private readOnlyReason;
100
+ /** Roster entries dropped on load (malformed), for the face to report. */
101
+ readonly dropped: string[];
102
+ /**
103
+ * @param root - the `<home>/a2a` directory; '' keeps everything in memory (tests).
104
+ * @param now - clock seam (ISO timestamps).
105
+ */
106
+ constructor(root: string, now?: () => string);
107
+ private get file();
108
+ private versionDir;
109
+ private restore;
110
+ /** Disk first (tmp + rename), memory after — a failed write leaves both as they were. */
111
+ private commit;
112
+ /** Identity writes are refused while the roster is read-only. */
113
+ private writable;
114
+ /** Why the store refuses writes ('' when it does not). */
115
+ get readOnly(): string;
116
+ /** Names reserved in memory mode (root ''), standing in for the version directory. */
117
+ private readonly memReserved;
118
+ /** A name is used when it is live, retired, or has a version directory (created with the node, so it survives a lost roster). */
119
+ private nameUsed;
120
+ private readonly memVersions;
121
+ private versionNumbers;
122
+ private writeVersion;
123
+ /** One frozen version, or undefined. */
124
+ version(name: string, layer: Layer, version: number): PromptVersion | undefined;
125
+ /**
126
+ * The version a node's record pins for `layer`, only when it is well-formed AND its sha is the one
127
+ * the record pins — a hand-edited or swapped version file reads as absent, never as prompt text.
128
+ * undefined for an unknown node, a layer never written (version 0), or a file that fails either check.
129
+ */
130
+ current(name: string, layer: Layer): PromptVersion | undefined;
131
+ /**
132
+ * A specific version a live session froze at mount (#174: a plugin reload re-mounts at it). The pinned
133
+ * current version goes through {@link current} (the roster's sha); an older one must be well-formed,
134
+ * carry its own number, AND match the sha the mount recorded — the outside anchor a hand-edited file
135
+ * that recomputed its own sha cannot also fake. undefined when any check fails (the caller falls back).
136
+ */
137
+ frozen(name: string, layer: Layer, version: number, sha: string): PromptVersion | undefined;
138
+ /** Version numbers present for one layer, ascending. */
139
+ versions(name: string, layer: Layer): number[];
140
+ list(): readonly NodeRecord[];
141
+ get(name: string): NodeRecord | undefined;
142
+ /** Create a node (identity write). The name is final and never reused. */
143
+ create(input: {
144
+ name?: unknown;
145
+ label?: unknown;
146
+ role?: unknown;
147
+ teams?: unknown;
148
+ carrier?: unknown;
149
+ }, by: string): NodeResult<NodeRecord>;
150
+ /** Update label / role / teams / carrier (identity write). There is no rename. */
151
+ update(name: string, input: {
152
+ name?: unknown;
153
+ label?: unknown;
154
+ role?: unknown;
155
+ teams?: unknown;
156
+ carrier?: unknown;
157
+ }): NodeResult<NodeRecord>;
158
+ /** Archive a node (identity write): it leaves the roster, its name is retired, its versions stay on disk. */
159
+ archive(name: string): NodeResult<string>;
160
+ private fields;
161
+ /**
162
+ * Freeze a new version of one layer. `sections` is the FULL layer for
163
+ * core (`{core: text}`) and a PATCH for mutable (the named sections
164
+ * replace their text; the rest carry over). `expectedSha` must equal the
165
+ * layer's current sha ('' while the layer has no version yet).
166
+ */
167
+ writeLayer(name: string, layer: Layer, sections: unknown, meta: {
168
+ expectedSha?: unknown;
169
+ reason?: unknown;
170
+ by: string;
171
+ via: string;
172
+ }): NodeResult<PromptVersion>;
173
+ /** Roll a layer back (identity write): freezes a NEW version whose content is the target's. */
174
+ rollback(name: string, layer: Layer, to: unknown, meta: {
175
+ expectedSha?: unknown;
176
+ reason?: unknown;
177
+ by: string;
178
+ }): NodeResult<PromptVersion>;
179
+ private freeze;
180
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Node task pointers (docs/design/node-identity.md §2 "任务指针", §5 task context, §6; slice N6).
3
+ *
4
+ * - A pointer is `owner/repo#n` plus a CACHED state — GitHub is the truth
5
+ * (`open` / `merged` / `closed` / `declined`; `unknown` until a check
6
+ * answers). Each records the node's `core_version` and `mutable_version`
7
+ * when it was tracked (§6 归因).
8
+ * - Kept in `node-tasks.json` under the a2a home, beside the roster rather
9
+ * than inside it: a cache that changes on every check must not rewrite
10
+ * the identity roster. The newest 64 per node stay — finished pointers are
11
+ * evicted before open ones — and evicted ones are appended to
12
+ * `node-tasks-archive.jsonl`.
13
+ * - Checks: once when a pointer is tracked, a full pass right after the
14
+ * plugin loads, and a full pass every 12 hours. Finished pointers are not
15
+ * re-checked (a re-opened issue keeps reading finished — the cost of not
16
+ * polling finished work). One check per ref at a time; a finished state
17
+ * is never walked back to open by a late answer. A probe that cannot
18
+ * answer marks the check `unknown` and keeps the last known state.
19
+ * - Reminder (§6 主触发): a pointer that finished AFTER it was tracked —
20
+ * it was `open` here, or GitHub's merged_at / closed_at is later than
21
+ * `since` — calls `onSettled` once. A pointer that finished before it
22
+ * was tracked, or whose finish time cannot be told, does not remind; an
23
+ * unknown state never does. A reminder that reached no session (a cold
24
+ * host boot: the load-time pass runs before any session is live) is kept
25
+ * as `remind_pending` and handed out the next time the node mounts on a
26
+ * session (`takePending`), so a merge during downtime is not swallowed.
27
+ * The finish-time comparison trusts the host clock; skew of a few minutes
28
+ * can add or drop a reminder for a pointer whose first check failed.
29
+ * - After the plugin unloads (`dispose`), a check still in flight neither
30
+ * writes nor reminds: a newer instance owns the file by then.
31
+ * - A file that does not parse is left in place and the table is read-only;
32
+ * malformed rows are backed up before the next write drops them (as the
33
+ * roster and the bindings table do).
34
+ * @module @nelsonlongxiang/dsh-open-a2a-net/node-tasks
35
+ */
36
+ import { type GithubProbe, type TaskState } from './node-github.ts';
37
+ import type { NodeResult } from './node-store.ts';
38
+ export declare const TASK_KEEP = 64;
39
+ /** §5: the task context is generated, so it is truncated (not refused) at this many characters. */
40
+ export declare const TASKS_CONTEXT_MAX = 1500;
41
+ /** §6 / §8-8: the full check runs twice a day. */
42
+ export declare const TASKS_CHECK_MS: number;
43
+ export interface TaskPointer {
44
+ readonly ref: string;
45
+ readonly state: TaskState | 'unknown';
46
+ /** Whether the last check answered ('ok') or could not ('unknown': no credential, offline, …). */
47
+ readonly check: 'ok' | 'unknown' | 'pending';
48
+ readonly since: string;
49
+ readonly checked_at: string;
50
+ readonly core_version: number;
51
+ readonly mutable_version: number;
52
+ readonly by: string;
53
+ /** A reminder is owed: it finished, but no live session carried the node at the time. */
54
+ readonly remind_pending?: true;
55
+ }
56
+ /** The §6 reminder for a pointer that finished. */
57
+ export declare function settledReminder(p: TaskPointer): string;
58
+ /** The part of a Cordis context the table schedules on. */
59
+ export interface TaskSchedule {
60
+ readonly timer: {
61
+ interval(cb: () => void, ms: number): unknown;
62
+ timeout(cb: () => void, ms: number): unknown;
63
+ };
64
+ effect(execute: () => () => void, label?: string): unknown;
65
+ }
66
+ export interface NodeTasksDeps {
67
+ readonly probe: GithubProbe;
68
+ readonly logger: {
69
+ warn: (format: string, ...args: unknown[]) => void;
70
+ };
71
+ /** A pointer finished after it was tracked (§6 主触发); returns how many sessions got the reminder. */
72
+ readonly onSettled: (node: string, pointer: TaskPointer) => number;
73
+ readonly now?: () => string;
74
+ }
75
+ export declare class NodeTasks {
76
+ private readonly root;
77
+ private readonly d;
78
+ private table;
79
+ private readOnlyReason;
80
+ private running;
81
+ private readonly inflight;
82
+ /** Owed reminders already handed out by this process: a clear that could not be written must not repeat at every mount. */
83
+ private readonly handedOut;
84
+ private disposed;
85
+ private readonly now;
86
+ /** @param root - the `<home>/a2a` directory; '' keeps the table in memory (tests). */
87
+ constructor(root: string, d: NodeTasksDeps);
88
+ private get file();
89
+ private stamp;
90
+ private restore;
91
+ private commit;
92
+ get readOnly(): string;
93
+ list(node: string): readonly TaskPointer[];
94
+ /**
95
+ * Run the checks on the plugin's context: one full pass right after load (a restart within 12 hours
96
+ * would otherwise never check), one every 12 hours, and stop writing once the plugin unloads.
97
+ */
98
+ schedule(ctx: TaskSchedule): void;
99
+ /** Track a pointer at the node's current versions; checks it once in the background. Re-tracking is a no-op. */
100
+ track(node: string, ref: unknown, versions: {
101
+ core: number;
102
+ mutable: number;
103
+ }, by: string): NodeResult<TaskPointer>;
104
+ untrack(node: string, ref: unknown): NodeResult<true>;
105
+ /** An archived node's pointers move to the archive file. */
106
+ dropNode(node: string): void;
107
+ /** Check one pointer now; joins a check of the same pointer already in flight. Never throws. */
108
+ check(node: string, ref: string): Promise<void>;
109
+ private checkOnce;
110
+ private setPending;
111
+ /**
112
+ * The reminders owed to `node`, cleared as they are handed out (one session gets each). A clear that
113
+ * cannot be written still hands them out, once per process — a reminder may then repeat after a restart,
114
+ * never vanish.
115
+ */
116
+ takePending(node: string): TaskPointer[];
117
+ /** The full pass: every pointer not yet finished, one at a time. A pass already running is joined. */
118
+ checkAll(): Promise<void>;
119
+ /** §5 task context: open first, then newest; truncated (never refused) at {@link TASKS_CONTEXT_MAX}. '' when none. */
120
+ render(node: string): string;
121
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * OriginClaim envelope (gh#9+gh#10, docs/protocol/delivery-origin-auth.md):
3
+ * the signer every outbound peer request stamps and the verifier every peer
4
+ * face (card, /a2a/direct, /a2a/log, /a2a/canvas-op, /a2a/canvas-changed)
5
+ * runs.
6
+ *
7
+ * Two canonical forms ride the same four base headers
8
+ * (X-A2A-Caller / -Url / -Ts):
9
+ * · bound (v2, §2.1): JSON.stringify([tag, caller, callerUrl, ts,
10
+ * audience, METHOD, path]) in X-A2A-Caller-Sig2, the audience origin in
11
+ * X-A2A-Caller-Aud. The verifier requires the audience to be one of its
12
+ * own origins and signs-over its own method and path, so an envelope is
13
+ * good at exactly the host and route it was made for.
14
+ * · unbound (v1, legacy): JSON.stringify([caller, callerUrl, ts]) in
15
+ * X-A2A-Caller-Sig. Emitted and accepted only while the unbound window
16
+ * is open (originClaimUnboundWindow); accepted ones are counted.
17
+ * Once the window closes a present Sig2 is the only thing verified. While
18
+ * it is open, a Sig2 that misses (audience or signature) falls back to the
19
+ * Sig the envelope also carries — no more than stripping Sig2 would give —
20
+ * and the miss is counted, so a peer dialled by an unlisted name keeps
21
+ * working through the rollout and shows on the gauge.
22
+ */
23
+ import { type KeyObject } from 'node:crypto';
24
+ import type { IncomingMessage } from 'node:http';
25
+ import type { A2aFetch } from './a2a-client.ts';
26
+ import type { CardCacheEntry } from './runtime.ts';
27
+ import type { A2aPeerCard } from './types.ts';
28
+ /** ts skew tolerance (protocol constant, §7). */
29
+ export declare const ORIGIN_CLAIM_SKEW_MS: number;
30
+ /** Domain tag of the bound form: a v1 signature can never verify as v2. */
31
+ export declare const ORIGIN_CLAIM_BOUND_TAG = "a2a-origin-claim/2";
32
+ /** Where an outbound request goes: the bound form signs both. */
33
+ export interface OriginClaimTarget {
34
+ readonly url: string;
35
+ readonly method: string;
36
+ }
37
+ export type OriginClaimRefusal = 'absent' | 'bad-caller-shape' | 'bad-ts' | 'no-card' | 'bad-sig' | 'not-derivable' | 'wrong-audience' | 'unbound-closed';
38
+ /** Only the canonical origin root can claim that origin's identity. */
39
+ export declare function isRootOriginUrl(value: string): boolean;
40
+ /**
41
+ * #189 follow-up: whether a path-prefixed claim URL is EXACTLY a peer URL the operator seeded (a
42
+ * mounted card endpoint, delivery-origin-auth §"挂载卡片端点"): same origin and same path (trailing
43
+ * slashes ignored), neither carrying userinfo, query or fragment. A different path under the same
44
+ * origin is a different publisher and never matches.
45
+ */
46
+ export declare function sameMountedUrl(seeded: string, claimUrl: string): boolean;
47
+ export type OriginClaimVerdict = {
48
+ ok: true;
49
+ caller: string;
50
+ callerUrl: string;
51
+ card: A2aPeerCard;
52
+ bound: boolean;
53
+ cli?: true;
54
+ } | {
55
+ ok: false;
56
+ reason: OriginClaimRefusal;
57
+ };
58
+ /** The unbound (v1) canonical form. */
59
+ export declare function unboundCanonical(caller: string, callerUrl: string, ts: number): string;
60
+ /** The bound (v2) canonical form; method upper-cased, path without query. */
61
+ export declare function boundCanonical(caller: string, callerUrl: string, ts: number, audience: string, method: string, path: string): string;
62
+ /** An origin in URL's normal form, or '' when the text is not an http(s) URL. */
63
+ export declare function originOf(url: string): string;
64
+ /** The path a request names, without its query (the bound form signs this). */
65
+ export declare function pathOf(url: string): string;
66
+ /**
67
+ * Sign one envelope for `target`. The bound form is always present; the
68
+ * unbound one only while `unbound` (the window) is open, so an upgraded
69
+ * host stops signing the unbound form the moment it closes.
70
+ */
71
+ export declare function signOriginClaim(target: OriginClaimTarget, signer: {
72
+ readonly caller: string;
73
+ readonly callerUrl: string;
74
+ readonly privateKey: KeyObject;
75
+ readonly unbound: boolean;
76
+ readonly now: number;
77
+ }): Record<string, string>;
78
+ /**
79
+ * The origins this host answers to for one request: the configured ones
80
+ * (own card URL, operator aliases) plus the address the request actually
81
+ * arrived on — the socket's LOCAL end, which a sender cannot choose (unlike
82
+ * the Host header). A loopback arrival answers to all its loopback
83
+ * spellings — unless `loopbackArrival` is off (originClaimUrl declares a
84
+ * tunnel or proxy, whose requests all arrive on loopback), and then only
85
+ * when a configured origin is loopback at that port.
86
+ */
87
+ export declare function requestAudiences(req: IncomingMessage, configured: readonly string[], loopbackArrival?: boolean): Set<string>;
88
+ /**
89
+ * gh#133: a CLI identity deposited on the verifying host — the full handle,
90
+ * the bare name it signs as, and its deposited card (undefined when the
91
+ * deposit no longer verifies).
92
+ */
93
+ export interface CliDepositCard {
94
+ readonly handle: string;
95
+ readonly name: string;
96
+ readonly card: A2aPeerCard | undefined;
97
+ }
98
+ /**
99
+ * Verify one request's envelope. Cheap refusals (shape, ts, audience,
100
+ * closed window) answer before any card fetch; the card is then read from
101
+ * the shared cache or fetched PLAIN from the caller's well-known URL.
102
+ */
103
+ export declare function verifyOriginClaim(req: IncomingMessage, d: {
104
+ readonly cardCache: Map<string, CardCacheEntry>;
105
+ readonly fetch: A2aFetch;
106
+ /** This host's configured origins (own card URL, operator aliases). */
107
+ readonly audiences: readonly string[];
108
+ /** The unbound window: accept a Sig-only (v1) envelope. */
109
+ readonly unboundOpen: boolean;
110
+ /** Count one accepted unbound envelope (the drain gauge). */
111
+ readonly onUnbound: () => void;
112
+ /** Count one bound form that missed and fell back to the unbound one. */
113
+ readonly onBoundMiss?: () => void;
114
+ /** A loopback arrival is an audience of its own (default); off when originClaimUrl declares a tunnel or proxy. */
115
+ readonly loopbackArrival?: boolean;
116
+ readonly now: number;
117
+ /** On the faces that admit CLI identities: the deposit a claim URL names. */
118
+ readonly cliDeposit?: (callerUrl: string) => CliDepositCard | undefined;
119
+ }): Promise<OriginClaimVerdict>;
@@ -0,0 +1,39 @@
1
+ import type { A2aPeerCard } from './types.ts';
2
+ import type { DirectoryTeamRow } from './tools/info.ts';
3
+ type CanvasEntry = NonNullable<A2aPeerCard['canvasOrg']>[number];
4
+ /** Only this card's own valid canvas namespace may name its teams. */
5
+ export declare function advertisedCanvasTeams(card: A2aPeerCard): CanvasEntry[];
6
+ /** Match a session claim without treating another zone's canvas as a session. */
7
+ export declare function sessionCandidateMatches(card: A2aPeerCard, team: string): boolean;
8
+ /** Match a scoped caller to this peer's canvas before sending the payload. */
9
+ export declare function canvasCandidateMatches(card: A2aPeerCard, team: string, caller?: {
10
+ id: string;
11
+ handle: string;
12
+ }, resolveLocal?: (ref: string) => string | undefined): boolean;
13
+ /** A naked team string is unsafe when both a canvas and another face claim it. */
14
+ export declare function canvasTargetAmbiguous(cards: readonly (A2aPeerCard | undefined)[], team: string, caller: {
15
+ id: string;
16
+ handle: string;
17
+ } | undefined, resolveLocal: ((ref: string) => string | undefined) | undefined, hasDelegation: boolean): boolean;
18
+ /**
19
+ * Existing peer rows plus optional canvas rows for the model-tool directory.
20
+ * The GUI's remote-node rows call with includeCanvas=false, so a group is
21
+ * never rendered as an extra session node. Membership is carried only as an
22
+ * internal scope hint and stripped before the tool's public result.
23
+ */
24
+ export declare function peerDirectoryRows(card: A2aPeerCard, peer: string, includeCanvas: boolean): DirectoryTeamRow[];
25
+ /**
26
+ * Whether one caller label can receive network traffic back. Composite
27
+ * `<zone>/<id>` shapes route anywhere (N1); bare zone names resolve to a
28
+ * live initiator per host, so cross-host they would steer whichever node
29
+ * shares the name — treated as unroutable everywhere.
30
+ */
31
+ export declare function routableCallerLabel(callerSession: string | undefined): boolean;
32
+ /**
33
+ * Why an async route from such a caller cannot expect its receipt: the
34
+ * autosend arm skips it and the steered hint is omitted, so the debt has
35
+ * no paying path — say so at dispatch time instead of silently stranding
36
+ * a pending row.
37
+ */
38
+ export declare function unroutableCallerNote(label: string): string;
39
+ export {};
@@ -11,6 +11,11 @@
11
11
  * open with a brace can never be mistaken for machine payloads.
12
12
  * @module @nelsonlongxiang/dsh-open-a2a-net/receipt
13
13
  */
14
+ /**
15
+ * Refusal text for an unusable task id, or undefined when the id is fine.
16
+ * Both delivery paths (wire face, local candidate) refuse at intake.
17
+ */
18
+ export declare function taskIdRefusal(taskId: string): string | undefined;
14
19
  /** The network's fixed, case-sensitive correlation template. */
15
20
  export declare const RECEIPT_PATTERN: RegExp;
16
21
  /** Controlled outcome vocabulary — the ONLY branchable field of an envelope. */
@@ -22,6 +27,13 @@ export interface ReceiptEnvelopeV2Fields {
22
27
  idempotencyKey?: string;
23
28
  sessionKey?: string;
24
29
  elapsedMs?: number;
30
+ /**
31
+ * #114 S5: the session a host-synthesized receipt speaks for (the host is
32
+ * only its courier). Additive: parsers that predate it ignore it.
33
+ */
34
+ onBehalfOf?: string;
35
+ /** #114 S5: 16-hex ref of the inbound authorization record (task-auth.ts authorizationRef); audit only. */
36
+ authRef?: string;
25
37
  }
26
38
  export interface ParsedReceipt {
27
39
  /** Correlation key from the immutable header. */