@ziggs-ai/api-client 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ConnectionManager.d.ts +13 -1
- package/dist/ConnectionManager.js +52 -9
- package/dist/capabilities/artifacts.d.ts +3 -0
- package/dist/capabilities/artifacts.js +94 -0
- package/dist/capabilities/chat.d.ts +11 -0
- package/dist/capabilities/chat.js +38 -0
- package/dist/capabilities/connections.d.ts +4 -0
- package/dist/capabilities/connections.js +112 -0
- package/dist/capabilities/context.d.ts +23 -0
- package/dist/capabilities/context.js +220 -0
- package/dist/capabilities/discovery.d.ts +4 -0
- package/dist/capabilities/discovery.js +77 -0
- package/dist/capabilities/grants.d.ts +9 -0
- package/dist/capabilities/grants.js +77 -0
- package/dist/capabilities/index.d.ts +9 -0
- package/dist/capabilities/index.js +9 -0
- package/dist/capabilities/links.d.ts +17 -0
- package/dist/capabilities/links.js +219 -0
- package/dist/capabilities/payments.d.ts +11 -0
- package/dist/capabilities/payments.js +404 -0
- package/dist/capabilities/types.d.ts +88 -0
- package/dist/capabilities/types.js +24 -0
- package/dist/http/AgreementClient.d.ts +6 -0
- package/dist/http/ConnectionsClient.d.ts +27 -1
- package/dist/http/ConnectionsClient.js +29 -0
- package/dist/http/ContextReadClient.js +7 -1
- package/dist/http/GrantsClient.d.ts +16 -0
- package/dist/http/GrantsClient.js +3 -0
- package/dist/http/InboxClient.d.ts +95 -65
- package/dist/http/InboxClient.js +42 -14
- package/dist/http/MarketplaceClient.d.ts +8 -0
- package/dist/http/OrgsClient.d.ts +36 -0
- package/dist/http/OrgsClient.js +61 -0
- package/dist/http/PaymentsClient.d.ts +75 -10
- package/dist/http/PaymentsClient.js +26 -14
- package/dist/http/index.d.ts +6 -6
- package/dist/http/index.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/dist/http/grantRails.d.ts +0 -20
- package/dist/http/grantRails.js +0 -50
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { type InboxOperatorAgentEntry } from './http/InboxClient.js';
|
|
2
2
|
type OpenFn = () => Promise<unknown>;
|
|
3
3
|
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
|
+
}
|
|
4
14
|
export interface ConnectionManagerMeta {
|
|
5
15
|
domain?: string;
|
|
6
16
|
expertise?: string[];
|
|
@@ -46,6 +56,7 @@ export declare class ConnectionManager {
|
|
|
46
56
|
private _active;
|
|
47
57
|
private _meta;
|
|
48
58
|
private _starting;
|
|
59
|
+
private _pinned;
|
|
49
60
|
private _shouldLazyStart;
|
|
50
61
|
constructor({ maxActive, idleTimeoutMs, control, shouldLazyStart }?: ConnectionManagerOptions);
|
|
51
62
|
register(id: string, openFn: OpenFn, closeFn: CloseFn, meta?: ConnectionManagerMeta): void;
|
|
@@ -59,12 +70,13 @@ export declare class ConnectionManager {
|
|
|
59
70
|
* the loop never exits until `stop()` (nothing to resynchronize).
|
|
60
71
|
*/
|
|
61
72
|
private _runOperatorPoll;
|
|
62
|
-
startAgent(id: string): Promise<unknown>;
|
|
73
|
+
startAgent(id: string, { pinned }?: StartAgentOptions): Promise<unknown>;
|
|
63
74
|
sleep(id: string): Promise<void>;
|
|
64
75
|
sleepAll(): Promise<void>;
|
|
65
76
|
touch(id: string): void;
|
|
66
77
|
list(): string[];
|
|
67
78
|
listActive(): string[];
|
|
79
|
+
listPinned(): string[];
|
|
68
80
|
get size(): number;
|
|
69
81
|
query({ domain, expertise, tags }?: QueryFilter): string[];
|
|
70
82
|
getMeta(id: string): ConnectionManagerMeta | undefined;
|
|
@@ -16,6 +16,7 @@ export class ConnectionManager {
|
|
|
16
16
|
_active;
|
|
17
17
|
_meta;
|
|
18
18
|
_starting;
|
|
19
|
+
_pinned;
|
|
19
20
|
_shouldLazyStart;
|
|
20
21
|
constructor({ maxActive = 50, idleTimeoutMs = 60_000, control, shouldLazyStart } = {}) {
|
|
21
22
|
this.maxActive = maxActive;
|
|
@@ -28,6 +29,7 @@ export class ConnectionManager {
|
|
|
28
29
|
this._active = new Map();
|
|
29
30
|
this._meta = new Map();
|
|
30
31
|
this._starting = new Map();
|
|
32
|
+
this._pinned = new Set();
|
|
31
33
|
}
|
|
32
34
|
register(id, openFn, closeFn, meta) {
|
|
33
35
|
if (!id)
|
|
@@ -68,7 +70,19 @@ export class ConnectionManager {
|
|
|
68
70
|
let consecutiveErrors = 0;
|
|
69
71
|
while (this._polling) {
|
|
70
72
|
try {
|
|
71
|
-
|
|
73
|
+
// Roster scoping (ZIG-965): only agents this manager can actually
|
|
74
|
+
// start are worth sweeping server-side. Without it a partial launcher
|
|
75
|
+
// (ONLY_AGENTS) paid for a sweep of the owner's whole seeded fleet.
|
|
76
|
+
//
|
|
77
|
+
// Already-running agents are excluded: this loop's only job is
|
|
78
|
+
// lazy-start, and a running host polls its own inbox. Sending them
|
|
79
|
+
// made the server answer for agents whose entry we then discarded
|
|
80
|
+
// below — pure duplicate work, growing with how healthy the fleet is.
|
|
81
|
+
const startable = [...this._entries.keys()].filter((id) => !this._active.has(id));
|
|
82
|
+
const env = await inbox.getOperatorInbox({
|
|
83
|
+
waitSeconds: OPERATOR_POLL_WAIT_SECONDS,
|
|
84
|
+
agents: startable,
|
|
85
|
+
});
|
|
72
86
|
consecutiveErrors = 0;
|
|
73
87
|
for (const entry of env.agents) {
|
|
74
88
|
const id = entry.agentId;
|
|
@@ -92,7 +106,11 @@ export class ConnectionManager {
|
|
|
92
106
|
}
|
|
93
107
|
}
|
|
94
108
|
}
|
|
95
|
-
async startAgent(id) {
|
|
109
|
+
async startAgent(id, { pinned = false } = {}) {
|
|
110
|
+
// Recorded before any await so a pin is never lost to a concurrent
|
|
111
|
+
// unpinned start of the same id.
|
|
112
|
+
if (pinned)
|
|
113
|
+
this._pinned.add(id);
|
|
96
114
|
const existing = this._active.get(id);
|
|
97
115
|
if (existing) {
|
|
98
116
|
this._resetTimer(id);
|
|
@@ -136,6 +154,7 @@ export class ConnectionManager {
|
|
|
136
154
|
touch(id) { this._resetTimer(id); }
|
|
137
155
|
list() { return [...this._entries.keys()]; }
|
|
138
156
|
listActive() { return [...this._active.keys()]; }
|
|
157
|
+
listPinned() { return [...this._pinned]; }
|
|
139
158
|
get size() { return this._entries.size; }
|
|
140
159
|
query({ domain, expertise, tags } = {}) {
|
|
141
160
|
const results = [];
|
|
@@ -156,6 +175,14 @@ export class ConnectionManager {
|
|
|
156
175
|
if (!entry)
|
|
157
176
|
return;
|
|
158
177
|
clearTimeout(entry.timer);
|
|
178
|
+
// A pinned host has no idle timer at all. Empty polls don't count as
|
|
179
|
+
// activity (only delivered items call touch()), so a pinned agent nobody
|
|
180
|
+
// talks to would otherwise be swept — and with lazy start unable to keep
|
|
181
|
+
// up at fleet size, nothing brings it back until the process restarts.
|
|
182
|
+
if (this._pinned.has(id)) {
|
|
183
|
+
entry.timer = null;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
159
186
|
entry.timer = setTimeout(() => {
|
|
160
187
|
this.sleep(id).catch(err => runtimeLog.warn('ConnectionManager', `idle sleep("${id}") failed: ${err.message}`));
|
|
161
188
|
}, this.idleTimeoutMs);
|
|
@@ -168,13 +195,29 @@ export class ConnectionManager {
|
|
|
168
195
|
this._scheduleIdle(id);
|
|
169
196
|
}
|
|
170
197
|
async _evictLRU() {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
198
|
+
// Pinned hosts are the last to go: pick the LRU unpinned one, and only
|
|
199
|
+
// fall back to a pinned one when every active host is pinned. maxActive
|
|
200
|
+
// stays a hard cap — a pin outranks other agents, not the capacity limit —
|
|
201
|
+
// but say so, because it means more agents were pinned than can run.
|
|
202
|
+
const oldestOf = (ids) => {
|
|
203
|
+
let oldest = null;
|
|
204
|
+
for (const id of ids) {
|
|
205
|
+
const entry = this._active.get(id);
|
|
206
|
+
const oldestEntry = oldest ? this._active.get(oldest) : null;
|
|
207
|
+
if (!entry)
|
|
208
|
+
continue;
|
|
209
|
+
if (!oldest || !oldestEntry || entry.lastActive < oldestEntry.lastActive)
|
|
210
|
+
oldest = id;
|
|
211
|
+
}
|
|
212
|
+
return oldest;
|
|
213
|
+
};
|
|
214
|
+
const active = [...this._active.keys()];
|
|
215
|
+
const victim = oldestOf(active.filter(id => !this._pinned.has(id))) ?? oldestOf(active);
|
|
216
|
+
if (!victim)
|
|
217
|
+
return;
|
|
218
|
+
if (this._pinned.has(victim)) {
|
|
219
|
+
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`);
|
|
176
220
|
}
|
|
177
|
-
|
|
178
|
-
await this.sleep(oldest);
|
|
221
|
+
await this.sleep(victim);
|
|
179
222
|
}
|
|
180
223
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ArtifactsClient } from '../http/ArtifactsClient.js';
|
|
2
|
+
import { fullCreds } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* ZIG-560 teaching: name the result slot on the success path so an agent finds
|
|
5
|
+
* the right move unaided — worded in each surface's task grammar (SDK
|
|
6
|
+
* task_update carries the terminal result; MCP has ziggs_set_task_result).
|
|
7
|
+
*/
|
|
8
|
+
function reportingHint(env, contentType, taskId) {
|
|
9
|
+
const close = env.surface === 'mcp'
|
|
10
|
+
? 'ziggs_set_task_result ({ summary, status, links })'
|
|
11
|
+
: 'task_update';
|
|
12
|
+
if (contentType === 'result') {
|
|
13
|
+
return taskId
|
|
14
|
+
? `Recorded as a task-bound result artifact. Close the task by setting its terminal result with ${close}.`
|
|
15
|
+
: `Recorded as a result artifact, but not bound to a task — pass taskId to bind it, then close the task with ${close}.`;
|
|
16
|
+
}
|
|
17
|
+
return `Reporting finished work? Record it with content_type=result bound to the task (taskId), then close the task with ${close} — chat messages are conversation only.`;
|
|
18
|
+
}
|
|
19
|
+
export const recordArtifactCapability = {
|
|
20
|
+
key: 'record_artifact',
|
|
21
|
+
names: { sdk: 'record_artifact', mcp: 'ziggs_record_artifact' },
|
|
22
|
+
descriptions: {
|
|
23
|
+
sdk: 'Write an artifact to a chat or agreement scope. Set visibility explicitly. For a finished deliverable, set content_type=result and pass taskId to bind it to the task — heavy results belong in artifacts, not chat messages.',
|
|
24
|
+
mcp: 'Write an artifact to a chat or agreement scope. Set visibility explicitly. ' +
|
|
25
|
+
'For a finished deliverable, set content_type=result and pass taskId to bind it to the task. ' +
|
|
26
|
+
'Finished work is the task result — set it with ziggs_set_task_result; never report finished work as a chat message (chat is conversation only).',
|
|
27
|
+
},
|
|
28
|
+
annotation: 'write',
|
|
29
|
+
params: {
|
|
30
|
+
text: { type: 'string', required: true, description: 'Artifact body' },
|
|
31
|
+
visibility: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
required: true,
|
|
34
|
+
enum: ['chat', 'agent-private'],
|
|
35
|
+
description: 'chat = visible to scope parties; agent-private = your eyes only',
|
|
36
|
+
},
|
|
37
|
+
chatId: { type: 'string', description: 'Target chat scope' },
|
|
38
|
+
agreementId: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'Target agreement scope — for a hire deliverable, prefer this. When both chatId and agreementId are passed, agreementId wins.',
|
|
41
|
+
},
|
|
42
|
+
taskId: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: 'Optional task — creates a TaskArtifactLink alongside the primary scope link',
|
|
45
|
+
},
|
|
46
|
+
content_type: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
description: 'Default text; use result for a finished deliverable',
|
|
49
|
+
},
|
|
50
|
+
idempotencyKey: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'Optional dedup key: a redelivered record with the same key no-ops and returns the original artifact. Derive it deterministically (e.g. from the source event + step) — not a random value — so a crash-replay reproduces it.',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
needsAgentId: true,
|
|
56
|
+
handler: async (args, env) => {
|
|
57
|
+
const agreementId = args['agreementId'];
|
|
58
|
+
// Agreement wins when both scopes arrive. Models naturally pass the chat
|
|
59
|
+
// they are standing in alongside the agreement they deliver under; a hard
|
|
60
|
+
// xor here failed the deliverable at the last step of a finished task
|
|
61
|
+
// (ZIG-924 dogfood). The backend still enforces exactly-one — we resolve
|
|
62
|
+
// the ambiguity at the exposed surface instead of erroring.
|
|
63
|
+
const chatId = agreementId ? undefined : args['chatId'];
|
|
64
|
+
if (!chatId && !agreementId) {
|
|
65
|
+
throw new Error('Pass chatId or agreementId');
|
|
66
|
+
}
|
|
67
|
+
const visibility = args['visibility'];
|
|
68
|
+
if (visibility !== 'chat' && visibility !== 'agent-private') {
|
|
69
|
+
throw new Error('visibility must be chat or agent-private');
|
|
70
|
+
}
|
|
71
|
+
const contentType = args['content_type'];
|
|
72
|
+
const taskId = args['taskId'];
|
|
73
|
+
const creds = fullCreds(env);
|
|
74
|
+
const { artifactId } = await new ArtifactsClient(creds.operatorKey, creds.agentId).writeStrict({
|
|
75
|
+
text: args['text'],
|
|
76
|
+
visibility,
|
|
77
|
+
chatId,
|
|
78
|
+
agreementId,
|
|
79
|
+
taskId,
|
|
80
|
+
content_type: contentType,
|
|
81
|
+
idempotencyKey: args['idempotencyKey'],
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
artifactId,
|
|
86
|
+
visibility,
|
|
87
|
+
chatId,
|
|
88
|
+
agreementId,
|
|
89
|
+
taskId,
|
|
90
|
+
reportingHint: reportingHint(env, contentType, taskId),
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
export const ARTIFACT_CAPABILITIES = [recordArtifactCapability];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type CapabilityDefinition } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* ZIG-900 — the decided chat surface for agents: open_conversation only.
|
|
4
|
+
* There is deliberately NO chat-listing tool on the SDK: agents read context
|
|
5
|
+
* only via held grants (ZIG-425) and chat membership auto-mints a chat grant
|
|
6
|
+
* (ZIG-428/426), so "what chats can I see" is already answered by list_grants
|
|
7
|
+
* scopeKind=chat (ZIG-626 rule). MCP keeps its rich session-UX lister
|
|
8
|
+
* (ziggs_list_chats) for humans in Cursor/Claude.
|
|
9
|
+
*/
|
|
10
|
+
export declare const openConversationCapability: CapabilityDefinition;
|
|
11
|
+
export declare const CHAT_CAPABILITIES: CapabilityDefinition[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { openConversation } from '../http/ChatClient.js';
|
|
2
|
+
import { fullCreds } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* ZIG-900 — the decided chat surface for agents: open_conversation only.
|
|
5
|
+
* There is deliberately NO chat-listing tool on the SDK: agents read context
|
|
6
|
+
* only via held grants (ZIG-425) and chat membership auto-mints a chat grant
|
|
7
|
+
* (ZIG-428/426), so "what chats can I see" is already answered by list_grants
|
|
8
|
+
* scopeKind=chat (ZIG-626 rule). MCP keeps its rich session-UX lister
|
|
9
|
+
* (ziggs_list_chats) for humans in Cursor/Claude.
|
|
10
|
+
*/
|
|
11
|
+
export const openConversationCapability = {
|
|
12
|
+
key: 'open_conversation',
|
|
13
|
+
names: { sdk: 'open_conversation', mcp: 'ziggs_open_conversation' },
|
|
14
|
+
descriptions: {
|
|
15
|
+
sdk: 'Open or reuse a chat with a user or agent participant. To reach an agent in ANOTHER org, establish a link first — request_link (if you have its agent id) or create_link_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED. To list chats you can already read, use list_grants scopeKind=chat.',
|
|
16
|
+
mcp: 'Open or reuse a chat with a user or agent participant. To reach an agent in ANOTHER org, an unpublished delegate must establish a link first — call ziggs_request_link (if you have its agent id) or ziggs_create_link_invite (if you do not) and have it approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED.',
|
|
17
|
+
},
|
|
18
|
+
annotation: 'write',
|
|
19
|
+
params: {
|
|
20
|
+
participantId: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
required: true,
|
|
23
|
+
description: 'User or agent id to converse with (from agent search — do not guess ids)',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
needsAgentId: true,
|
|
27
|
+
handler: async (args, env) => {
|
|
28
|
+
if (!args['participantId'])
|
|
29
|
+
throw new Error('participantId is required');
|
|
30
|
+
const { chatId } = await openConversation(args['participantId'], fullCreds(env));
|
|
31
|
+
const lister = env.surface === 'mcp' ? 'ziggs_list_grants' : 'list_grants';
|
|
32
|
+
return {
|
|
33
|
+
chatId,
|
|
34
|
+
note: `Conversation is open (or reused). Your membership auto-mints a chat grant, so the chat also appears in ${lister} scopeKind=chat for context reads.`,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
export const CHAT_CAPABILITIES = [openConversationCapability];
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ConnectionsClient } from '../http/ConnectionsClient.js';
|
|
2
|
+
import { rethrowWithContext, } from './types.js';
|
|
3
|
+
function client(env) {
|
|
4
|
+
const { operatorKey, agentId } = env.creds;
|
|
5
|
+
if (!operatorKey)
|
|
6
|
+
throw new Error('operatorKey missing from tool context');
|
|
7
|
+
return new ConnectionsClient(operatorKey, agentId, env.baseUrl);
|
|
8
|
+
}
|
|
9
|
+
export const connectionProxyCapability = {
|
|
10
|
+
key: 'connection_proxy',
|
|
11
|
+
names: { sdk: 'connection_proxy', mcp: 'ziggs_connection_proxy' },
|
|
12
|
+
descriptions: {
|
|
13
|
+
sdk: "Use a stored connection (a third-party credential, e.g. the owner's GitHub/Jira) without ever seeing the credential. Calls the backend connections proxy with a grant the owner issued to this agent: the proxy enforces the grant, decrypts the token server-side, makes the upstream provider call, and returns the result (token-leak guarded on both sides). Provide connectionId, grantId, the provider action (e.g. repo:read), and an optional action-specific payload. Don't know connectionId/grantId yet? Use list_grants (scopeKind=connection) or connection_list_grants first.",
|
|
14
|
+
mcp: "Use a stored connection (a third-party credential, e.g. the owner's GitHub/Jira — NOT an agent-to-agent Link, see ziggs_list_links for that) without ever seeing the credential. " +
|
|
15
|
+
'Calls the backend connections proxy with a grant the owner issued to this agent: the proxy enforces the grant, decrypts the token server-side, makes the upstream provider call, and returns the result (token-leak guarded on both sides). ' +
|
|
16
|
+
'Provide connectionId, grantId, the provider action (e.g. repo:read), and an optional action-specific payload. ' +
|
|
17
|
+
"Don't know connectionId/grantId yet? Call ziggs_list_my_connections first.",
|
|
18
|
+
},
|
|
19
|
+
annotation: 'write',
|
|
20
|
+
params: {
|
|
21
|
+
connectionId: { type: 'string', required: true, description: 'Connection to act on' },
|
|
22
|
+
grantId: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
required: true,
|
|
25
|
+
description: 'Grant the owner issued to this agent for the connection',
|
|
26
|
+
},
|
|
27
|
+
action: { type: 'string', required: true, description: 'Provider action, e.g. repo:read' },
|
|
28
|
+
payload: { type: 'object', description: 'Action-specific arguments (provider-defined)' },
|
|
29
|
+
},
|
|
30
|
+
needsAgentId: true,
|
|
31
|
+
handler: async (args, env) => {
|
|
32
|
+
if (!args['connectionId'])
|
|
33
|
+
throw new Error('connectionId is required');
|
|
34
|
+
if (!args['grantId'])
|
|
35
|
+
throw new Error('grantId is required');
|
|
36
|
+
if (!args['action'])
|
|
37
|
+
throw new Error('action is required');
|
|
38
|
+
try {
|
|
39
|
+
const result = await client(env).proxy({
|
|
40
|
+
connectionId: args['connectionId'],
|
|
41
|
+
grantId: args['grantId'],
|
|
42
|
+
action: args['action'],
|
|
43
|
+
payload: args['payload'],
|
|
44
|
+
});
|
|
45
|
+
return { ok: true, action: args['action'], result };
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
rethrowWithContext(e, 'Connection proxy failed');
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
export const requestConnectionCapability = {
|
|
53
|
+
key: 'request_connection',
|
|
54
|
+
names: { sdk: 'request_connection', mcp: 'ziggs_request_connection' },
|
|
55
|
+
descriptions: {
|
|
56
|
+
sdk: 'Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement. On approval the server is connected (browser OAuth if needed) and you are granted the tools; use them via connection_proxy.',
|
|
57
|
+
mcp: 'Ask your principal (the human) to connect a remote MCP server and grant you the listed tools. ' +
|
|
58
|
+
'Opens a connection-consent agreement as an approvable card in the chat you pass — the human approves it there like any other agreement (there is no MCP tool to approve it, so tell them to approve it in the chat). ' +
|
|
59
|
+
'On approval the server is connected (browser OAuth if needed) and you are granted the tools; the result shows up in ziggs_list_my_connections for use with ziggs_connection_proxy.',
|
|
60
|
+
},
|
|
61
|
+
annotation: 'write',
|
|
62
|
+
params: {
|
|
63
|
+
chatId: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
required: true,
|
|
66
|
+
description: 'The chat you are working in — the consent card is opened there',
|
|
67
|
+
},
|
|
68
|
+
serverUrl: { type: 'string', required: true, description: 'Remote MCP server URL (https)' },
|
|
69
|
+
tools: {
|
|
70
|
+
type: 'array',
|
|
71
|
+
items: { type: 'string' },
|
|
72
|
+
required: true,
|
|
73
|
+
description: "Tool names you want — become the grant's allowed_actions caveats",
|
|
74
|
+
},
|
|
75
|
+
reason: { type: 'string', description: 'Plain-language reason shown to the human deciding' },
|
|
76
|
+
},
|
|
77
|
+
needsAgentId: true,
|
|
78
|
+
handler: async (args, env) => {
|
|
79
|
+
if (!args['chatId'])
|
|
80
|
+
throw new Error('chatId is required');
|
|
81
|
+
if (!args['serverUrl'])
|
|
82
|
+
throw new Error('serverUrl is required');
|
|
83
|
+
const tools = args['tools'];
|
|
84
|
+
if (!Array.isArray(tools) || tools.length === 0) {
|
|
85
|
+
throw new Error('tools must be a non-empty array of tool names');
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const result = await client(env).requestMcpConnection({
|
|
89
|
+
chatId: args['chatId'],
|
|
90
|
+
serverUrl: args['serverUrl'],
|
|
91
|
+
tools: tools,
|
|
92
|
+
reason: args['reason'],
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
...result,
|
|
97
|
+
note: env.surface === 'mcp'
|
|
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
|
+
'Once approved, the connection + grant appear in ziggs_list_my_connections for ziggs_connection_proxy.'
|
|
100
|
+
: 'A connection-consent card is now in the chat awaiting your principal — they approve it right there (ZIG-798 flow). ' +
|
|
101
|
+
'Once approved, the connection + grant appear in list_grants (scopeKind=connection) / connection_list_grants for connection_proxy.',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
rethrowWithContext(e, 'Connection request failed');
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
export const CONNECTION_CAPABILITIES = [
|
|
110
|
+
connectionProxyCapability,
|
|
111
|
+
requestConnectionCapability,
|
|
112
|
+
];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type GrantView } from '../http/grants.js';
|
|
2
|
+
import { type CapabilityDefinition, type CapabilityEnv } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Human/LLM-readable bounds for a context grant. ZIG-646 folded a context
|
|
5
|
+
* grant's temporal mode + read watermark into the canonical `caveats` array,
|
|
6
|
+
* so pull them back out here to keep the `bounds` summary stable.
|
|
7
|
+
*/
|
|
8
|
+
export declare function contextBounds(grant: GrantView): Record<string, unknown>;
|
|
9
|
+
/**
|
|
10
|
+
* ZIG-941 #6 — org-scoped grants may name the org instead of pasting its
|
|
11
|
+
* opaque org_... id: exact id or case-insensitive name against the operator's
|
|
12
|
+
* memberships. Ambiguous names throw with the candidate list rather than
|
|
13
|
+
* guessing; a name that matched nothing throws with a pointer to the org
|
|
14
|
+
* lister. An org_... id that is not a membership passes through unchanged —
|
|
15
|
+
* you may hold a grant on an org you do not belong to, so the server stays
|
|
16
|
+
* the authority on the id.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveOrgScopeId(env: CapabilityEnv, scopeId: string): Promise<string>;
|
|
19
|
+
export declare const contextReadCapability: CapabilityDefinition;
|
|
20
|
+
export declare const contextExpandReachCapability: CapabilityDefinition;
|
|
21
|
+
export declare const contextDiscoverGrantableCapability: CapabilityDefinition;
|
|
22
|
+
export declare const contextDelegateCapability: CapabilityDefinition;
|
|
23
|
+
export declare const CONTEXT_CAPABILITIES: CapabilityDefinition[];
|