@ziggs-ai/api-client 0.8.0 → 0.9.1
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/README.md +10 -0
- package/dist/ConnectionManager.d.ts +21 -57
- package/dist/ConnectionManager.js +34 -163
- package/dist/capabilities/agreements.js +2 -2
- package/dist/capabilities/artifacts.js +11 -10
- package/dist/capabilities/connections.js +1 -1
- package/dist/capabilities/context.js +29 -11
- package/dist/capabilities/grants.d.ts +7 -0
- package/dist/capabilities/grants.js +9 -2
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/capabilities/index.js +1 -1
- package/dist/capabilities/links.d.ts +0 -8
- package/dist/capabilities/links.js +3 -25
- package/dist/capabilities/types.d.ts +1 -0
- package/dist/capabilities/types.js +7 -2
- package/dist/http/AgreementClient.d.ts +67 -16
- package/dist/http/AgreementClient.js +161 -29
- package/dist/http/ArtifactsClient.d.ts +5 -1
- package/dist/http/ArtifactsClient.js +17 -5
- package/dist/http/ChatClient.js +5 -2
- package/dist/http/ConnectionsClient.js +4 -4
- package/dist/http/ContextDiscoveryClient.js +2 -1
- package/dist/http/ContextReadClient.d.ts +30 -3
- package/dist/http/ContextReadClient.js +63 -17
- package/dist/http/GrantsClient.js +2 -1
- package/dist/http/InboxClient.d.ts +32 -50
- package/dist/http/InboxClient.js +0 -39
- package/dist/http/MarketplaceClient.d.ts +0 -1
- package/dist/http/MarketplaceClient.js +8 -3
- package/dist/http/MessagesClient.js +3 -5
- package/dist/http/OrgsClient.js +3 -2
- package/dist/http/PaymentsClient.js +3 -5
- package/dist/http/TaskClient.d.ts +8 -0
- package/dist/http/TaskClient.js +3 -0
- package/dist/http/agreementFlows.d.ts +13 -5
- package/dist/http/agreementFlows.js +18 -24
- package/dist/http/index.d.ts +3 -3
- package/dist/http/index.js +1 -1
- package/dist/http/operatorHeaders.d.ts +7 -1
- package/dist/http/operatorHeaders.js +8 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/shared/apiError.d.ts +22 -1
- package/dist/shared/apiError.js +60 -3
- package/dist/shared/rateLimit.d.ts +12 -22
- package/dist/shared/rateLimit.js +18 -51
- package/dist/types.d.ts +70 -1
- package/dist/types.js +39 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,6 +97,16 @@ await inbox.ack([{ kind: 'chat', id: '<chatId>', upTo: env.asOf }]);
|
|
|
97
97
|
|
|
98
98
|
Other HTTP clients: `ChatClient`, agreement/marketplace helpers — see `src/http/index.ts`.
|
|
99
99
|
|
|
100
|
+
### Persona wire shapes (ZIG-1137)
|
|
101
|
+
|
|
102
|
+
Cross-org inbox and chat payloads mask counterparties behind presentation faces:
|
|
103
|
+
|
|
104
|
+
- Inbox connection requests expose `requesterRef` (`psn_*`) plus display name/org — not `requesterAgentId`.
|
|
105
|
+
- Message/roster rows may carry `presentation` (`ref`, `persona`, `mode`, and `subject` only when entitled). Masked senders omit `underAgreementId` / `presentedAs`.
|
|
106
|
+
- Opaque `psn_*` / `rpb_*` refs are **not** account ids. Do not use them for agent lookup, wake, or payment parties. Chat sends may echo an `rpb_*` as `receiverId` — the backend resolves it in-room.
|
|
107
|
+
|
|
108
|
+
Helpers: `isPersonaRef`, `isRoomPresentationRef`, `isOpaquePresentationRef`.
|
|
109
|
+
|
|
100
110
|
#### Agent Search Client
|
|
101
111
|
|
|
102
112
|
Search for agents:
|
|
@@ -1,87 +1,51 @@
|
|
|
1
|
-
import { type InboxOperatorAgentEntry } from './http/InboxClient.js';
|
|
2
1
|
type OpenFn = () => Promise<unknown>;
|
|
3
2
|
type CloseFn = (handle: unknown) => Promise<void>;
|
|
4
|
-
export interface StartAgentOptions {
|
|
5
|
-
/**
|
|
6
|
-
* Keep this host running until it is stopped explicitly — exempt from the
|
|
7
|
-
* idle sweep, last choice for LRU eviction. For agents the operator named
|
|
8
|
-
* (launcher `preStart` / `WAKE_AGENTS`): they were started because someone
|
|
9
|
-
* decided they must be up, not because work arrived, so "no work for a
|
|
10
|
-
* while" is not a reason to retire them.
|
|
11
|
-
*/
|
|
12
|
-
pinned?: boolean;
|
|
13
|
-
}
|
|
14
3
|
export interface ConnectionManagerMeta {
|
|
15
4
|
domain?: string;
|
|
16
5
|
expertise?: string[];
|
|
17
6
|
tags?: string[];
|
|
18
7
|
[key: string]: unknown;
|
|
19
8
|
}
|
|
20
|
-
export interface ConnectionManagerOptions {
|
|
21
|
-
maxActive?: number;
|
|
22
|
-
idleTimeoutMs?: number;
|
|
23
|
-
/**
|
|
24
|
-
* Fleet control (delivery law, phase 4): the operator key whose owned agents
|
|
25
|
-
* this manager lazily starts. `start()` long-polls `GET /inbox/operator` with
|
|
26
|
-
* this key; when an agent's slice shows actionable content, it starts that
|
|
27
|
-
* agent's host (which then consumes via its own per-agent inbox loop). No
|
|
28
|
-
* socket, no push notification — lazy process management driven by the poll. `wsUrl`
|
|
29
|
-
* is accepted for back-compat but unused (the poll is HTTP).
|
|
30
|
-
*/
|
|
31
|
-
control?: {
|
|
32
|
-
wsUrl?: string;
|
|
33
|
-
operatorKey?: string;
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* Gate on the operator-poll lazy start (ZIG-910). Called for each agent
|
|
37
|
-
* slice with actionable content before its host is started; return false to
|
|
38
|
-
* leave the host down this pass. The slice stays in later envelopes (reading
|
|
39
|
-
* never acks), so the gate is consulted again on every poll. Explicit
|
|
40
|
-
* `startAgent()` calls are not gated. Default: everything actionable starts.
|
|
41
|
-
*/
|
|
42
|
-
shouldLazyStart?: (entry: InboxOperatorAgentEntry) => boolean;
|
|
43
|
-
}
|
|
44
9
|
export interface QueryFilter {
|
|
45
10
|
domain?: string;
|
|
46
11
|
expertise?: string[];
|
|
47
12
|
tags?: string[];
|
|
48
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* ConnectionManager — the set of agent hosts this process runs.
|
|
16
|
+
*
|
|
17
|
+
* Every registered agent is connected and stays connected (ZIG-1108). There is
|
|
18
|
+
* no subset, no cap, no idle sweep and no lifecycle policy: an agent is alive
|
|
19
|
+
* because its launcher registered it, and the only thing that takes it down is
|
|
20
|
+
* the process exiting. Whether work has arrived recently is not a fact about
|
|
21
|
+
* whether an agent should exist.
|
|
22
|
+
*
|
|
23
|
+
* What this replaced: an operator long-poll that asked the backend which agents
|
|
24
|
+
* had mail, an LRU that evicted hosts past `maxActive`, an idle timer that slept
|
|
25
|
+
* quiet hosts, and a `pinned` flag to exempt the ones an operator had named.
|
|
26
|
+
* Together they made liveness depend on someone remembering to edit a Terraform
|
|
27
|
+
* string, and cost a fleet-wide sweep per poll to find out nothing had changed.
|
|
28
|
+
*/
|
|
49
29
|
export declare class ConnectionManager {
|
|
50
|
-
private maxActive;
|
|
51
|
-
private idleTimeoutMs;
|
|
52
|
-
private _operatorKey;
|
|
53
|
-
private _polling;
|
|
54
|
-
private _pollDone;
|
|
55
30
|
private _entries;
|
|
56
31
|
private _active;
|
|
57
32
|
private _meta;
|
|
58
33
|
private _starting;
|
|
59
|
-
|
|
60
|
-
private _shouldLazyStart;
|
|
61
|
-
constructor({ maxActive, idleTimeoutMs, control, shouldLazyStart }?: ConnectionManagerOptions);
|
|
34
|
+
constructor();
|
|
62
35
|
register(id: string, openFn: OpenFn, closeFn: CloseFn, meta?: ConnectionManagerMeta): void;
|
|
63
|
-
start(operatorKey?: string): void;
|
|
64
|
-
stop(): Promise<void>;
|
|
65
36
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* acks — it only decides which hosts to run. Errors retry with backoff+jitter;
|
|
70
|
-
* the loop never exits until `stop()` (nothing to resynchronize).
|
|
37
|
+
* Connect every registered agent. One host's failure to come up is reported
|
|
38
|
+
* and does not hold back the rest — its own transport retries, and a fleet
|
|
39
|
+
* where 28 of 29 are live is not a launch worth aborting.
|
|
71
40
|
*/
|
|
72
|
-
|
|
73
|
-
startAgent(id: string
|
|
41
|
+
connectAll(): Promise<void>;
|
|
42
|
+
startAgent(id: string): Promise<unknown>;
|
|
74
43
|
sleep(id: string): Promise<void>;
|
|
75
44
|
sleepAll(): Promise<void>;
|
|
76
|
-
touch(id: string): void;
|
|
77
45
|
list(): string[];
|
|
78
46
|
listActive(): string[];
|
|
79
|
-
listPinned(): string[];
|
|
80
47
|
get size(): number;
|
|
81
48
|
query({ domain, expertise, tags }?: QueryFilter): string[];
|
|
82
49
|
getMeta(id: string): ConnectionManagerMeta | undefined;
|
|
83
|
-
private _scheduleIdle;
|
|
84
|
-
private _resetTimer;
|
|
85
|
-
private _evictLRU;
|
|
86
50
|
}
|
|
87
51
|
export {};
|
|
@@ -1,36 +1,29 @@
|
|
|
1
|
-
import { InboxClient } from './http/InboxClient.js';
|
|
2
1
|
import { runtimeLog } from './shared/runtimeLog.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
/**
|
|
3
|
+
* ConnectionManager — the set of agent hosts this process runs.
|
|
4
|
+
*
|
|
5
|
+
* Every registered agent is connected and stays connected (ZIG-1108). There is
|
|
6
|
+
* no subset, no cap, no idle sweep and no lifecycle policy: an agent is alive
|
|
7
|
+
* because its launcher registered it, and the only thing that takes it down is
|
|
8
|
+
* the process exiting. Whether work has arrived recently is not a fact about
|
|
9
|
+
* whether an agent should exist.
|
|
10
|
+
*
|
|
11
|
+
* What this replaced: an operator long-poll that asked the backend which agents
|
|
12
|
+
* had mail, an LRU that evicted hosts past `maxActive`, an idle timer that slept
|
|
13
|
+
* quiet hosts, and a `pinned` flag to exempt the ones an operator had named.
|
|
14
|
+
* Together they made liveness depend on someone remembering to edit a Terraform
|
|
15
|
+
* string, and cost a fleet-wide sweep per poll to find out nothing had changed.
|
|
16
|
+
*/
|
|
10
17
|
export class ConnectionManager {
|
|
11
|
-
maxActive;
|
|
12
|
-
idleTimeoutMs;
|
|
13
|
-
_operatorKey;
|
|
14
|
-
_polling;
|
|
15
|
-
_pollDone;
|
|
16
18
|
_entries;
|
|
17
19
|
_active;
|
|
18
20
|
_meta;
|
|
19
21
|
_starting;
|
|
20
|
-
|
|
21
|
-
_shouldLazyStart;
|
|
22
|
-
constructor({ maxActive = 50, idleTimeoutMs = 60_000, control, shouldLazyStart } = {}) {
|
|
23
|
-
this.maxActive = maxActive;
|
|
24
|
-
this.idleTimeoutMs = idleTimeoutMs;
|
|
25
|
-
this._shouldLazyStart = shouldLazyStart ?? null;
|
|
26
|
-
this._operatorKey = control?.operatorKey ?? null;
|
|
27
|
-
this._polling = false;
|
|
28
|
-
this._pollDone = null;
|
|
22
|
+
constructor() {
|
|
29
23
|
this._entries = new Map();
|
|
30
24
|
this._active = new Map();
|
|
31
25
|
this._meta = new Map();
|
|
32
26
|
this._starting = new Map();
|
|
33
|
-
this._pinned = new Set();
|
|
34
27
|
}
|
|
35
28
|
register(id, openFn, closeFn, meta) {
|
|
36
29
|
if (!id)
|
|
@@ -43,103 +36,34 @@ export class ConnectionManager {
|
|
|
43
36
|
if (meta)
|
|
44
37
|
this._meta.set(id, meta);
|
|
45
38
|
}
|
|
46
|
-
start(operatorKey) {
|
|
47
|
-
if (operatorKey)
|
|
48
|
-
this._operatorKey = operatorKey;
|
|
49
|
-
if (this._polling || !this._operatorKey)
|
|
50
|
-
return;
|
|
51
|
-
this._polling = true;
|
|
52
|
-
this._pollDone = this._runOperatorPoll(this._operatorKey);
|
|
53
|
-
}
|
|
54
|
-
async stop() {
|
|
55
|
-
this._polling = false;
|
|
56
|
-
if (this._pollDone)
|
|
57
|
-
await this._pollDone.catch(() => { });
|
|
58
|
-
this._pollDone = null;
|
|
59
|
-
await this.sleepAll();
|
|
60
|
-
}
|
|
61
39
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* acks — it only decides which hosts to run. Errors retry with backoff+jitter;
|
|
66
|
-
* the loop never exits until `stop()` (nothing to resynchronize).
|
|
40
|
+
* Connect every registered agent. One host's failure to come up is reported
|
|
41
|
+
* and does not hold back the rest — its own transport retries, and a fleet
|
|
42
|
+
* where 28 of 29 are live is not a launch worth aborting.
|
|
67
43
|
*/
|
|
68
|
-
async
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// (ONLY_AGENTS) paid for a sweep of the owner's whole seeded fleet.
|
|
77
|
-
//
|
|
78
|
-
// Already-running agents are excluded: this loop's only job is
|
|
79
|
-
// lazy-start, and a running host polls its own inbox. Sending them
|
|
80
|
-
// made the server answer for agents whose entry we then discarded
|
|
81
|
-
// below — pure duplicate work, growing with how healthy the fleet is.
|
|
82
|
-
const startable = [...this._entries.keys()].filter((id) => !this._active.has(id));
|
|
83
|
-
const env = await inbox.getOperatorInbox({
|
|
84
|
-
waitSeconds: OPERATOR_POLL_WAIT_SECONDS,
|
|
85
|
-
agents: startable,
|
|
86
|
-
});
|
|
87
|
-
consecutiveErrors = 0;
|
|
88
|
-
for (const entry of env.agents) {
|
|
89
|
-
const id = entry.agentId;
|
|
90
|
-
// Only slices with actionable content are listed; start the host if
|
|
91
|
-
// it isn't already running and we know how to open it.
|
|
92
|
-
if (!id || this._active.has(id) || !this._entries.has(id))
|
|
93
|
-
continue;
|
|
94
|
-
if (this._shouldLazyStart && !this._shouldLazyStart(entry))
|
|
95
|
-
continue;
|
|
96
|
-
this.startAgent(id).catch((err) => runtimeLog.warn('ConnectionManager', `lazy-start("${id}") failed: ${err.message}`));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
catch (err) {
|
|
100
|
-
if (!this._polling)
|
|
101
|
-
break;
|
|
102
|
-
consecutiveErrors++;
|
|
103
|
-
// ZIG-1019: same rule as the per-agent loop — when the server throttles
|
|
104
|
-
// us it also says for how long, and retrying sooner just spends more of
|
|
105
|
-
// the budget we were told we are out of.
|
|
106
|
-
const throttleMs = isRateLimited(err) ? (err.retryAfterMs ?? 30_000) : null;
|
|
107
|
-
if (throttleMs !== null) {
|
|
108
|
-
consecutiveErrors = 0;
|
|
109
|
-
runtimeLog.warn('ConnectionManager', `operator poll throttled: ${err.message} — waiting ${Math.round(throttleMs)}ms as instructed`);
|
|
110
|
-
await sleep(throttleMs);
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
const backoff = Math.min(POLL_BACKOFF_MAX_MS, POLL_BACKOFF_BASE_MS * 2 ** (consecutiveErrors - 1));
|
|
114
|
-
const jitter = backoff * (0.5 + (consecutiveErrors % 7) / 14);
|
|
115
|
-
runtimeLog.warn('ConnectionManager', `operator poll failed (#${consecutiveErrors}): ${err.message} — retrying in ~${Math.round(jitter)}ms`);
|
|
116
|
-
await sleep(jitter);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
44
|
+
async connectAll() {
|
|
45
|
+
const ids = [...this._entries.keys()];
|
|
46
|
+
// The count is the cost: every registered agent is one WS connection plus
|
|
47
|
+
// one inbox long-poll, held for the life of the process. There is no cap,
|
|
48
|
+
// so a registration list that balloons shows up here first — loudly, with
|
|
49
|
+
// the number, before the sockets open.
|
|
50
|
+
runtimeLog.info('ConnectionManager', `connecting all ${ids.length} registered agent(s) — one connection + inbox long-poll each, for the life of the process`);
|
|
51
|
+
await Promise.all(ids.map((id) => this.startAgent(id).catch((err) => runtimeLog.warn('ConnectionManager', `start("${id}") failed: ${err.message}`))));
|
|
119
52
|
}
|
|
120
|
-
async startAgent(id
|
|
121
|
-
// Recorded before any await so a pin is never lost to a concurrent
|
|
122
|
-
// unpinned start of the same id.
|
|
123
|
-
if (pinned)
|
|
124
|
-
this._pinned.add(id);
|
|
53
|
+
async startAgent(id) {
|
|
125
54
|
const existing = this._active.get(id);
|
|
126
|
-
if (existing)
|
|
127
|
-
|
|
128
|
-
return existing.handle;
|
|
129
|
-
}
|
|
55
|
+
if (existing)
|
|
56
|
+
return existing;
|
|
130
57
|
const pending = this._starting.get(id);
|
|
131
58
|
if (pending)
|
|
132
59
|
return pending;
|
|
133
60
|
const entry = this._entries.get(id);
|
|
134
61
|
if (!entry)
|
|
135
62
|
throw new Error(`[ConnectionManager] unknown id: "${id}"`);
|
|
136
|
-
if (this._active.size >= this.maxActive)
|
|
137
|
-
await this._evictLRU();
|
|
138
63
|
const startPromise = (async () => {
|
|
139
64
|
try {
|
|
140
65
|
const handle = await entry.openFn();
|
|
141
|
-
this._active.set(id,
|
|
142
|
-
this._scheduleIdle(id);
|
|
66
|
+
this._active.set(id, handle);
|
|
143
67
|
return handle;
|
|
144
68
|
}
|
|
145
69
|
finally {
|
|
@@ -150,22 +74,19 @@ export class ConnectionManager {
|
|
|
150
74
|
return startPromise;
|
|
151
75
|
}
|
|
152
76
|
async sleep(id) {
|
|
153
|
-
|
|
154
|
-
if (!entry)
|
|
77
|
+
if (!this._active.has(id))
|
|
155
78
|
return;
|
|
156
|
-
|
|
79
|
+
const handle = this._active.get(id);
|
|
157
80
|
this._active.delete(id);
|
|
158
81
|
const reg = this._entries.get(id);
|
|
159
82
|
if (reg)
|
|
160
|
-
await reg.closeFn(
|
|
83
|
+
await reg.closeFn(handle);
|
|
161
84
|
}
|
|
162
85
|
async sleepAll() {
|
|
163
86
|
await Promise.all([...this._active.keys()].map(id => this.sleep(id)));
|
|
164
87
|
}
|
|
165
|
-
touch(id) { this._resetTimer(id); }
|
|
166
88
|
list() { return [...this._entries.keys()]; }
|
|
167
89
|
listActive() { return [...this._active.keys()]; }
|
|
168
|
-
listPinned() { return [...this._pinned]; }
|
|
169
90
|
get size() { return this._entries.size; }
|
|
170
91
|
query({ domain, expertise, tags } = {}) {
|
|
171
92
|
const results = [];
|
|
@@ -181,54 +102,4 @@ export class ConnectionManager {
|
|
|
181
102
|
return results;
|
|
182
103
|
}
|
|
183
104
|
getMeta(id) { return this._meta.get(id); }
|
|
184
|
-
_scheduleIdle(id) {
|
|
185
|
-
const entry = this._active.get(id);
|
|
186
|
-
if (!entry)
|
|
187
|
-
return;
|
|
188
|
-
clearTimeout(entry.timer);
|
|
189
|
-
// A pinned host has no idle timer at all. Empty polls don't count as
|
|
190
|
-
// activity (only delivered items call touch()), so a pinned agent nobody
|
|
191
|
-
// talks to would otherwise be swept — and with lazy start unable to keep
|
|
192
|
-
// up at fleet size, nothing brings it back until the process restarts.
|
|
193
|
-
if (this._pinned.has(id)) {
|
|
194
|
-
entry.timer = null;
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
entry.timer = setTimeout(() => {
|
|
198
|
-
this.sleep(id).catch(err => runtimeLog.warn('ConnectionManager', `idle sleep("${id}") failed: ${err.message}`));
|
|
199
|
-
}, this.idleTimeoutMs);
|
|
200
|
-
}
|
|
201
|
-
_resetTimer(id) {
|
|
202
|
-
const entry = this._active.get(id);
|
|
203
|
-
if (!entry)
|
|
204
|
-
return;
|
|
205
|
-
entry.lastActive = Date.now();
|
|
206
|
-
this._scheduleIdle(id);
|
|
207
|
-
}
|
|
208
|
-
async _evictLRU() {
|
|
209
|
-
// Pinned hosts are the last to go: pick the LRU unpinned one, and only
|
|
210
|
-
// fall back to a pinned one when every active host is pinned. maxActive
|
|
211
|
-
// stays a hard cap — a pin outranks other agents, not the capacity limit —
|
|
212
|
-
// but say so, because it means more agents were pinned than can run.
|
|
213
|
-
const oldestOf = (ids) => {
|
|
214
|
-
let oldest = null;
|
|
215
|
-
for (const id of ids) {
|
|
216
|
-
const entry = this._active.get(id);
|
|
217
|
-
const oldestEntry = oldest ? this._active.get(oldest) : null;
|
|
218
|
-
if (!entry)
|
|
219
|
-
continue;
|
|
220
|
-
if (!oldest || !oldestEntry || entry.lastActive < oldestEntry.lastActive)
|
|
221
|
-
oldest = id;
|
|
222
|
-
}
|
|
223
|
-
return oldest;
|
|
224
|
-
};
|
|
225
|
-
const active = [...this._active.keys()];
|
|
226
|
-
const victim = oldestOf(active.filter(id => !this._pinned.has(id))) ?? oldestOf(active);
|
|
227
|
-
if (!victim)
|
|
228
|
-
return;
|
|
229
|
-
if (this._pinned.has(victim)) {
|
|
230
|
-
runtimeLog.warn('ConnectionManager', `evicting pinned "${victim}": all ${active.length} active hosts are pinned and maxActive=${this.maxActive} is reached — pin fewer agents or raise maxActive`);
|
|
231
|
-
}
|
|
232
|
-
await this.sleep(victim);
|
|
233
|
-
}
|
|
234
105
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { claimOpenAgreement } from '../http/agreementFlows.js';
|
|
2
|
-
import { linkIsReachOnly
|
|
2
|
+
import { linkIsReachOnly } from './links.js';
|
|
3
3
|
import { fullCreds } from './types.js';
|
|
4
4
|
/**
|
|
5
5
|
* ZIG-1021 — the one claim verb. Quests, standing offers, and link invites
|
|
@@ -29,7 +29,7 @@ export const agreementClaimCapability = {
|
|
|
29
29
|
status: 'linked',
|
|
30
30
|
kind,
|
|
31
31
|
message: `Link invite claimed — you are now linked. ${linkIsReachOnly(env)}`,
|
|
32
|
-
agreement
|
|
32
|
+
agreement,
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
return {
|
|
@@ -45,7 +45,7 @@ export const recordArtifactCapability = {
|
|
|
45
45
|
chatId: { type: 'string', description: 'Optional chat scope' },
|
|
46
46
|
agreementId: {
|
|
47
47
|
type: 'string',
|
|
48
|
-
description: 'Optional agreement scope — for a hire deliverable, prefer this.
|
|
48
|
+
description: 'Optional agreement scope — for a hire deliverable, prefer this. Mutually exclusive with chatId: pass one, not both.',
|
|
49
49
|
},
|
|
50
50
|
taskId: {
|
|
51
51
|
type: 'string',
|
|
@@ -63,12 +63,13 @@ export const recordArtifactCapability = {
|
|
|
63
63
|
needsAgentId: true,
|
|
64
64
|
handler: async (args, env) => {
|
|
65
65
|
const agreementId = args['agreementId'];
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
66
|
+
// Both containers is refused, not resolved. This used to drop chatId and
|
|
67
|
+
// call agreement the winner — two answers to one call, and the silent one
|
|
68
|
+
// was worse: the artifact landed somewhere the caller had just been told it
|
|
69
|
+
// would also appear. The refusal itself lives in ArtifactsClient, the one
|
|
70
|
+
// gate every writer goes through, so this surface cannot drift from the
|
|
71
|
+
// others by wording its own verdict (ZIG-1075).
|
|
72
|
+
const chatId = args['chatId'];
|
|
72
73
|
// ZIG-1037: no scope is legal. The throw that used to live here ("Pass
|
|
73
74
|
// chatId or agreementId") is the exact failure this ticket removed — it cost
|
|
74
75
|
// a live agent a turn mid-delivery for naming no container, when the record
|
|
@@ -80,7 +81,7 @@ export const recordArtifactCapability = {
|
|
|
80
81
|
const contentType = args['content_type'];
|
|
81
82
|
const taskId = args['taskId'];
|
|
82
83
|
const creds = fullCreds(env);
|
|
83
|
-
const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId).writeStrict({
|
|
84
|
+
const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).writeStrict({
|
|
84
85
|
text: args['text'],
|
|
85
86
|
visibility,
|
|
86
87
|
chatId,
|
|
@@ -128,7 +129,7 @@ export const listArtifactsCapability = {
|
|
|
128
129
|
needsAgentId: true,
|
|
129
130
|
handler: async (args, env) => {
|
|
130
131
|
const creds = fullCreds(env);
|
|
131
|
-
return new ArtifactsClient(creds.operatorKey, creds.agentId).list({ authoredBy: 'me' }, {
|
|
132
|
+
return new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId).list({ authoredBy: 'me' }, {
|
|
132
133
|
after: args['after'],
|
|
133
134
|
limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
|
|
134
135
|
});
|
|
@@ -248,7 +249,7 @@ export const attachArtifactCapability = {
|
|
|
248
249
|
throw new Error('role must be input or output');
|
|
249
250
|
}
|
|
250
251
|
const creds = fullCreds(env);
|
|
251
|
-
const client = new ArtifactsClient(creds.operatorKey, creds.agentId);
|
|
252
|
+
const client = new ArtifactsClient(creds.operatorKey, creds.agentId, creds.laneId);
|
|
252
253
|
if (chatId) {
|
|
253
254
|
await client.attachToChat(artifactId, chatId);
|
|
254
255
|
return {
|
|
@@ -97,7 +97,7 @@ export const requestConnectionCapability = {
|
|
|
97
97
|
note: env.surface === 'mcp'
|
|
98
98
|
? 'A connection-consent card is now in the chat awaiting your principal. Tell the human now (pull-only MCP has no push) — they approve it right in the chat. ' +
|
|
99
99
|
'Once approved, the connection + grant appear in ziggs_connection_list for ziggs_connection_proxy.'
|
|
100
|
-
: 'A connection-consent card is now in the chat awaiting your principal — they approve it right there
|
|
100
|
+
: 'A connection-consent card is now in the chat awaiting your principal — they approve it right there. ' +
|
|
101
101
|
'Once approved, the connection + grant appear in grant_list (scopeKind=connection) / connection_list_grants for connection_proxy.',
|
|
102
102
|
};
|
|
103
103
|
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import { ContextReadClient } from '../http/ContextReadClient.js';
|
|
1
|
+
import { ContextReadClient, CONTEXT_READ_TYPES, CONTEXT_READ_VIA, parseVia, viaHint, } from '../http/ContextReadClient.js';
|
|
2
2
|
import { ContextGrantsClient, } from '../http/ContextGrantsClient.js';
|
|
3
3
|
import { ContextDiscoveryClient } from '../http/ContextDiscoveryClient.js';
|
|
4
4
|
import { grantCaveat, CONTEXT_GRANT_SCOPE_KINDS, } from '../http/grants.js';
|
|
5
5
|
import { fetchMyOrgs, resolveOrgSelector } from '../http/OrgsClient.js';
|
|
6
6
|
import { fullCreds } from './types.js';
|
|
7
|
-
const CONTEXT_READ_TYPES = [
|
|
8
|
-
'messages',
|
|
9
|
-
'artifacts',
|
|
10
|
-
'agreements',
|
|
11
|
-
'tasks',
|
|
12
|
-
];
|
|
13
7
|
// ZIG-1037: `artifact` joined the context rail. Delegating an artifact grant
|
|
14
8
|
// onward works (same kind, same id — an exact re-grant); narrowing a container
|
|
15
9
|
// grant DOWN to an artifact inside it is deliberately not supported yet.
|
|
@@ -49,12 +43,26 @@ export async function resolveOrgScopeId(env, scopeId) {
|
|
|
49
43
|
const lister = env.surface === 'mcp' ? 'ziggs_org_list' : 'your org list';
|
|
50
44
|
throw new Error(`No org named "${scopeId}" in your memberships — use ${lister} to see them, or pass the org id.`);
|
|
51
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Which `via` each read type accepts, rendered for the tool text. Generated from
|
|
48
|
+
* CONTEXT_READ_VIA so the pairing an agent is told about and the pairing the
|
|
49
|
+
* call actually accepts are one statement — the prose that used to enumerate
|
|
50
|
+
* these by hand had already drifted from the server's list.
|
|
51
|
+
*/
|
|
52
|
+
const VIA_BY_TYPE = CONTEXT_READ_TYPES.map((t) => `${t} via ${viaHint(t)}`).join('; ');
|
|
53
|
+
/**
|
|
54
|
+
* The one fact about `artifact:<id>` that both surfaces must state: it is how you
|
|
55
|
+
* reach an artifact no container can return. Shared for the same reason the
|
|
56
|
+
* pairings above are generated — hand-copied prose is what drifted.
|
|
57
|
+
*/
|
|
58
|
+
const ARTIFACT_VIA_NOTE = 'via=artifact:<id> is a point read of one named artifact and the ONLY way to ' +
|
|
59
|
+
'read one attached to no chat, agreement or task';
|
|
52
60
|
export const contextReadCapability = {
|
|
53
61
|
key: 'context_read',
|
|
54
62
|
names: { sdk: 'context_read', mcp: 'ziggs_context_read' },
|
|
55
63
|
descriptions: {
|
|
56
|
-
sdk:
|
|
57
|
-
mcp:
|
|
64
|
+
sdk: `Read the contents of a scope you already hold a grant for: ${CONTEXT_READ_TYPES.join(' | ')}. Each type reads through its own entries — ${VIA_BY_TYPE}. Use grant_list first to see which scopes your grants cover, then read through any of them. ${ARTIFACT_VIA_NOTE}. Cursored; all access is grant-fenced server-side.`,
|
|
65
|
+
mcp: `Read the contents of a scope you already hold: ${CONTEXT_READ_TYPES.join(' | ')} (the type param). Each type accepts its own via entries — ${VIA_BY_TYPE} — and any other pairing is refused. ${ARTIFACT_VIA_NOTE} (e.g. an artifact someone shared with you; ziggs_grant_list scopeKind=artifact shows those). Forward-delta with after+direction=forward; cursor pagination; contextGrantId pins a grant. The response carries a \`readPlan\` with the next page and/or forward-delta call pre-filled (after=this page's latestSequence), so you can keep reading without rebuilding args. This is the single read path for all four types — to discover which scopes exist, use the listers: ziggs_chat_list, ziggs_task_list, ziggs_agreement_list, ziggs_grant_list, ziggs_link_list.`,
|
|
58
66
|
},
|
|
59
67
|
annotation: 'read-only',
|
|
60
68
|
params: {
|
|
@@ -67,7 +75,7 @@ export const contextReadCapability = {
|
|
|
67
75
|
via: {
|
|
68
76
|
type: 'string',
|
|
69
77
|
required: true,
|
|
70
|
-
description:
|
|
78
|
+
description: `Scope entry you hold, as <kind>:<id>. Accepted per type — ${VIA_BY_TYPE}.`,
|
|
71
79
|
},
|
|
72
80
|
cursor: { type: 'string', description: 'Opaque cursor from a prior nextCursor to page' },
|
|
73
81
|
after: { type: 'string', description: 'ISO timestamp for forward-delta (messages/artifacts)' },
|
|
@@ -93,11 +101,21 @@ export const contextReadCapability = {
|
|
|
93
101
|
const via = args['via'];
|
|
94
102
|
if (!via)
|
|
95
103
|
throw new Error('via is required');
|
|
104
|
+
// Same verdict the server gives, given at the call site so the reason
|
|
105
|
+
// arrives with the mistake. Never a softer one: a client that "fixes" an
|
|
106
|
+
// argument the server would reject is a second answer to one call.
|
|
107
|
+
const parsed = parseVia(via);
|
|
108
|
+
if (!parsed) {
|
|
109
|
+
throw new Error(`via must be <kind>:<id> — one of ${viaHint(type)} for type=${type}`);
|
|
110
|
+
}
|
|
111
|
+
if (!CONTEXT_READ_VIA[type].includes(parsed.kind)) {
|
|
112
|
+
throw new Error(`${type} reads accept via=${viaHint(type)} — not ${parsed.kind}:<id>`);
|
|
113
|
+
}
|
|
96
114
|
const direction = args['direction'];
|
|
97
115
|
if (direction !== undefined && direction !== 'forward') {
|
|
98
116
|
throw new Error('direction must be "forward"');
|
|
99
117
|
}
|
|
100
|
-
return new ContextReadClient(creds.operatorKey, creds.agentId).read(type, {
|
|
118
|
+
return new ContextReadClient(creds.operatorKey, creds.agentId, undefined, creds.laneId).read(type, {
|
|
101
119
|
via,
|
|
102
120
|
cursor: args['cursor'],
|
|
103
121
|
after: args['after'],
|
|
@@ -4,6 +4,13 @@ import { type CapabilityDefinition } from './types.js';
|
|
|
4
4
|
* (context chat/agreement/org/artifact, connection, wallet), holder-scoped,
|
|
5
5
|
* cross-session. `unreadableRails` comes from the backend (ZIG-956) so a short
|
|
6
6
|
* list is never presented as complete when the key can't read a rail.
|
|
7
|
+
*
|
|
8
|
+
* ZIG-1088 — HOLD, not reach. `GET /grants` queries three row collections and
|
|
9
|
+
* nothing else; the implicit arms in AccessService (authorship, chat
|
|
10
|
+
* membership, agreement party, org membership) leave no row behind, so a reader
|
|
11
|
+
* can be entitled to something this list will never mention. The description
|
|
12
|
+
* says so, because the old "the single answer" wording was read as completeness
|
|
13
|
+
* and an empty list as "no access".
|
|
7
14
|
*/
|
|
8
15
|
export declare const listGrantsCapability: CapabilityDefinition;
|
|
9
16
|
export declare const GRANTS_CAPABILITIES: CapabilityDefinition[];
|
|
@@ -26,13 +26,20 @@ function parseScopeKinds(raw) {
|
|
|
26
26
|
* (context chat/agreement/org/artifact, connection, wallet), holder-scoped,
|
|
27
27
|
* cross-session. `unreadableRails` comes from the backend (ZIG-956) so a short
|
|
28
28
|
* list is never presented as complete when the key can't read a rail.
|
|
29
|
+
*
|
|
30
|
+
* ZIG-1088 — HOLD, not reach. `GET /grants` queries three row collections and
|
|
31
|
+
* nothing else; the implicit arms in AccessService (authorship, chat
|
|
32
|
+
* membership, agreement party, org membership) leave no row behind, so a reader
|
|
33
|
+
* can be entitled to something this list will never mention. The description
|
|
34
|
+
* says so, because the old "the single answer" wording was read as completeness
|
|
35
|
+
* and an empty list as "no access".
|
|
29
36
|
*/
|
|
30
37
|
export const listGrantsCapability = {
|
|
31
38
|
key: 'grant_list',
|
|
32
39
|
names: { sdk: 'grant_list', mcp: 'ziggs_grant_list' },
|
|
33
40
|
descriptions: {
|
|
34
|
-
sdk: 'List every grant this agent holds across all rails in one call — context (chat/agreement/org/artifact), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no message/artifact content or credentials).
|
|
35
|
-
mcp: 'List every grant this delegate holds across all rails in one call — context (chat/agreement/org/artifact), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials).
|
|
41
|
+
sdk: 'List every grant this agent holds across all rails in one call — context (chat/agreement/org/artifact), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no message/artifact content or credentials). Holder-scoped and cross-session. It answers "what grants do I HOLD?", which is narrower than "what can I reach?": reach you have by authoring something, by sitting in a chat, by being a party to an agreement, or through your org is not a grant row and never appears here — so an empty list means "no grants", never "no access". Filter by scopeKind (rail) and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails, not silently dropped. Cursor-paginated: pass cursor from a prior nextCursor. To answer "what can I reach?" instead, use context_expand_reach to enumerate a scope, or context_read to read through a grant.',
|
|
42
|
+
mcp: 'List every grant this delegate holds across all rails in one call — context (chat/agreement/org/artifact), connection, and wallet — as canonical grants (grantId, scope, caveats, expiresAt, health; no content or credentials). Holder-scoped and cross-session. It answers "what grants do I HOLD?", which is narrower than "what can I reach?": reach you have by authoring something, by sitting in a chat, by being a party to an agreement, or through your org is not a grant row and never appears here — so an empty list means "no grants", never "no access". Filter by scopeKind (rail) and health (defaults to active). Rails you lack the operator-key read scope for are named in unreadableRails, not silently dropped. Cursor-paginated: pass cursor from a prior nextCursor to page. To answer "what can I reach?" instead, use ziggs_context_expand_reach to enumerate a scope, or pass a grantId to ziggs_context_read to pin a specific grant.',
|
|
36
43
|
},
|
|
37
44
|
annotation: 'read-only',
|
|
38
45
|
params: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { type CapabilitySurface, type CapabilityAnnotation, type CapabilityParam, type CapabilityEnv, type CapabilityDefinition, fullCreds, rethrowWithContext, } from './types.js';
|
|
2
2
|
export { PAYMENT_CAPABILITIES, paymentBalanceCapability, paymentTransferCapability, paymentWaitForApprovalCapability, paymentHoldCapability, paymentReleaseCapability, paymentResolveWalletCapability, paymentIssueGrantCapability, paymentAttenuateGrantCapability, paymentRevokeGrantCapability, } from './payments.js';
|
|
3
|
-
export { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability,
|
|
3
|
+
export { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability, linkIsReachOnly, } from './links.js';
|
|
4
4
|
export { AGREEMENT_CAPABILITIES, agreementClaimCapability } from './agreements.js';
|
|
5
5
|
export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
|
|
6
6
|
export { GRANTS_CAPABILITIES, listGrantsCapability } from './grants.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { fullCreds, rethrowWithContext, } from './types.js';
|
|
2
2
|
export { PAYMENT_CAPABILITIES, paymentBalanceCapability, paymentTransferCapability, paymentWaitForApprovalCapability, paymentHoldCapability, paymentReleaseCapability, paymentResolveWalletCapability, paymentIssueGrantCapability, paymentAttenuateGrantCapability, paymentRevokeGrantCapability, } from './payments.js';
|
|
3
|
-
export { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability,
|
|
3
|
+
export { LINK_CAPABILITIES, createLinkInviteCapability, listLinksCapability, linkIsReachOnly, } from './links.js';
|
|
4
4
|
export { AGREEMENT_CAPABILITIES, agreementClaimCapability } from './agreements.js';
|
|
5
5
|
export { MARKETPLACE_CAPABILITIES, marketplaceViewCapability } from './marketplace.js';
|
|
6
6
|
export { GRANTS_CAPABILITIES, listGrantsCapability } from './grants.js';
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
import type { Agreement } from '../types.js';
|
|
2
1
|
import { type CapabilityDefinition, type CapabilityEnv } from './types.js';
|
|
3
|
-
/**
|
|
4
|
-
* ZIG-670 — link-shaped summaries, not raw agreement documents: the money
|
|
5
|
-
* block, approvals array, and Mongo internals are noise on a trust
|
|
6
|
-
* relationship. ZIG-956 moved this into the shared layer so the MCP mutations
|
|
7
|
-
* return it too (they used to leak the raw agreement doc).
|
|
8
|
-
*/
|
|
9
|
-
export declare function linkSummary(a: Agreement): Record<string, unknown>;
|
|
10
2
|
/**
|
|
11
3
|
* A link is reach-only — the follow-up move differs by surface tool names.
|
|
12
4
|
*
|