@forloop-cc/forloop-opencode-plugin-planner 2.2.26 → 2.2.27
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@forloop-cc/forloop-opencode-plugin-planner",
|
|
4
|
-
"version": "2.2.
|
|
4
|
+
"version": "2.2.27",
|
|
5
5
|
"description": "ForLoop integration plugin for opencode — Sprint, story, and agent tools for the ForLoop platform",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -7,8 +7,15 @@ type RegistryEntry = {
|
|
|
7
7
|
agentId: string;
|
|
8
8
|
name: string;
|
|
9
9
|
serviceCode?: string | null;
|
|
10
|
+
authMode?: 'none' | 'app' | 'user';
|
|
10
11
|
};
|
|
11
12
|
|
|
13
|
+
// The target manifest is the source of truth for consent: a user-auth service
|
|
14
|
+
// only accepts a user-subject connection, so the planner must never create an
|
|
15
|
+
// app-subject request against it as a workaround.
|
|
16
|
+
const subjectTypeFor = (target: RegistryEntry): 'user' | 'app' =>
|
|
17
|
+
target.authMode === 'user' ? 'user' : 'app';
|
|
18
|
+
|
|
12
19
|
type RequestWire = {
|
|
13
20
|
id: string;
|
|
14
21
|
targetAgentId: string;
|
|
@@ -75,7 +82,7 @@ const requestLines = (request: RequestWire): string[] => {
|
|
|
75
82
|
export function createA2ARequestConnectionTool(client: ForLoopAPIClient) {
|
|
76
83
|
return tool({
|
|
77
84
|
description:
|
|
78
|
-
'Create (or idempotently reuse) a connection request for the current sprint: the app asks a service/peer for an exact scope set. The request is persisted and visible in App Connections, but the planner can never approve it — the signed-in owner must click Approve. Returns the request id and a deep link. Use before writing an integration brief that depends on an external service.',
|
|
85
|
+
'Create (or idempotently reuse) a connection request for the current sprint: the app asks a service/peer for an exact scope set. The request is persisted and visible in App Connections, but the planner can never approve it — the signed-in owner (or, for user-subject grants, the end user themselves) must click Approve. Returns the request id and a deep link. Use before writing an integration brief that depends on an external service. subjectType defaults to the target manifest authMode: "user" for user-owned data (the user consents), otherwise "app" for app-owned/standing integrations. Never force app for a user-auth target.',
|
|
79
86
|
args: {
|
|
80
87
|
agentId: tool.schema.string().describe('Target service/peer agentId (public id)'),
|
|
81
88
|
scopes: tool.schema
|
|
@@ -85,6 +92,10 @@ export function createA2ARequestConnectionTool(client: ForLoopAPIClient) {
|
|
|
85
92
|
.string()
|
|
86
93
|
.optional()
|
|
87
94
|
.describe('Why the app needs this connection (shown to the approving user)'),
|
|
95
|
+
subjectType: tool.schema
|
|
96
|
+
.enum(['user', 'app'])
|
|
97
|
+
.optional()
|
|
98
|
+
.describe('Connection subject: "user" for user-owned data (delegated user consent), "app" for an app-owned/standing integration. Defaults to the target authMode; an app subject is refused for a user-auth target.'),
|
|
88
99
|
},
|
|
89
100
|
async execute(args, context) {
|
|
90
101
|
const tokenResult = await validateToken();
|
|
@@ -107,23 +118,42 @@ export function createA2ARequestConnectionTool(client: ForLoopAPIClient) {
|
|
|
107
118
|
return `❌ No scopes requested for \`${args.agentId}\`. Nothing was created.`;
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
const targetSubject = subjectTypeFor(target)
|
|
122
|
+
if (targetSubject === 'user' && args.subjectType === 'app') {
|
|
123
|
+
return [
|
|
124
|
+
`❌ Target \`${args.agentId}\` requires user-auth (authMode: "user"); refusing to create an app-subject request.`,
|
|
125
|
+
'The end user must consent for their own data. Retry without subjectType (it defaults to "user" for this target) or pass subjectType: "user".',
|
|
126
|
+
].join('\n');
|
|
127
|
+
}
|
|
128
|
+
const subjectType = args.subjectType ?? targetSubject
|
|
110
129
|
const result = await client.createConnectionRequest({
|
|
111
130
|
sprintId,
|
|
112
131
|
targetAgentId: args.agentId,
|
|
113
132
|
scopes: requested,
|
|
114
|
-
subjectType
|
|
133
|
+
subjectType,
|
|
115
134
|
requestedBy: 'planner',
|
|
116
135
|
...(args.reason ? { reason: args.reason } : {}),
|
|
117
136
|
});
|
|
118
137
|
const request = result.request as RequestWire;
|
|
119
138
|
const dropped = Array.isArray(result.droppedScopes) ? result.droppedScopes : [];
|
|
120
139
|
|
|
140
|
+
if (subjectType === 'user' && request.subjectType !== 'user') {
|
|
141
|
+
return [
|
|
142
|
+
'❌ The platform did not create a user-subject connection request; nothing usable was created.',
|
|
143
|
+
`- returned subject: ${request.subjectType ?? 'unknown'}`,
|
|
144
|
+
'Do not use an app-subject request for user-owned data. Record `Connection: pending user consent (UI-only)` and stop until the platform delegated-user exchange and user-subject requests are available.',
|
|
145
|
+
].join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
121
148
|
return [
|
|
122
149
|
request.status === 'approved'
|
|
123
150
|
? '## Connection already approved'
|
|
124
|
-
:
|
|
151
|
+
: request.subjectType === 'user'
|
|
152
|
+
? "## Connection request created — waiting for the user's consent"
|
|
153
|
+
: '## Connection request created — waiting for the app owner',
|
|
125
154
|
'',
|
|
126
155
|
`- target: **${target.name}** (\`${target.agentId}\`)`,
|
|
156
|
+
`- request subject: ${request.subjectType === 'user' ? 'user-subject (user consent)' : 'app-subject (app owner)'}`,
|
|
127
157
|
...requestLines({ ...request, link: result.link ?? request.link }),
|
|
128
158
|
...(dropped.length
|
|
129
159
|
? ['', `⚠️ Dropped (not declared by the target manifest): ${dropped.join(', ')}`]
|
|
@@ -132,6 +162,7 @@ export function createA2ARequestConnectionTool(client: ForLoopAPIClient) {
|
|
|
132
162
|
'The planner cannot approve this. Ask the user to click **Approve** in App Connections, or call `a2a_open_connection_request` to bring the panel forward.',
|
|
133
163
|
'Do **not** write the connection id into a brief until `a2a_check_connections` reports an active connection — a pending request is not a grant.',
|
|
134
164
|
'If the user replies that they approved, call `a2a_check_connections` to read the real connection id and scopes.',
|
|
165
|
+
'For user-owned data the user consents in the platform panel; the app owner cannot consent for them.',
|
|
135
166
|
].join('\n');
|
|
136
167
|
} catch (error: any) {
|
|
137
168
|
return `❌ Error creating the connection request: ${error.message}`;
|
|
@@ -258,10 +289,16 @@ export function createA2AOpenConnectionRequestTool(client: ForLoopAPIClient) {
|
|
|
258
289
|
if (!args.agentId) {
|
|
259
290
|
return '❌ Provide a request id, or a target agentId (and scopes) so the request can be created.';
|
|
260
291
|
}
|
|
292
|
+
const target = await findEntry(client, args.agentId);
|
|
293
|
+
if (!target) {
|
|
294
|
+
return `❌ Target \`${args.agentId}\` was not found in the registry. Do not invent a connection request.`;
|
|
295
|
+
}
|
|
296
|
+
const desiredSubject = subjectTypeFor(target);
|
|
261
297
|
const state = (await client.getConnectionState(sprintId)) as ConnectionState;
|
|
262
298
|
const wanted = new Set((args.scopes ?? []).map(scope => scope.trim()).filter(Boolean));
|
|
263
299
|
const match = (state.requests ?? []).find(request => {
|
|
264
300
|
if (request.targetAgentId !== args.agentId) return false;
|
|
301
|
+
if (request.subjectType !== desiredSubject) return false;
|
|
265
302
|
if (request.status !== 'pending' && request.status !== 'approved') return false;
|
|
266
303
|
if (!wanted.size) return true;
|
|
267
304
|
return (
|
|
@@ -280,9 +317,16 @@ export function createA2AOpenConnectionRequestTool(client: ForLoopAPIClient) {
|
|
|
280
317
|
sprintId,
|
|
281
318
|
targetAgentId: args.agentId,
|
|
282
319
|
scopes: [...wanted],
|
|
283
|
-
subjectType:
|
|
320
|
+
subjectType: desiredSubject,
|
|
284
321
|
requestedBy: 'planner',
|
|
285
322
|
});
|
|
323
|
+
if (desiredSubject === 'user' && created.request?.subjectType !== 'user') {
|
|
324
|
+
return [
|
|
325
|
+
'❌ The platform did not create a user-subject connection request; nothing usable was created.',
|
|
326
|
+
`- returned subject: ${created.request?.subjectType ?? 'unknown'}`,
|
|
327
|
+
'Do not use an app-subject request for user-owned data. Record `Connection: pending user consent (UI-only)` and stop.',
|
|
328
|
+
].join('\n');
|
|
329
|
+
}
|
|
286
330
|
requestId = created.request?.id ?? '';
|
|
287
331
|
link = created.link ?? created.request?.link ?? null;
|
|
288
332
|
}
|
|
@@ -25,6 +25,12 @@ type RegistryEntry = {
|
|
|
25
25
|
directIntegrable?: boolean;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
// The target manifest is the source of truth for consent: a user-auth service
|
|
29
|
+
// only accepts a user-subject connection, so the planner must never create an
|
|
30
|
+
// app-subject request against it as a workaround.
|
|
31
|
+
const subjectTypeFor = (target: RegistryEntry): 'user' | 'app' =>
|
|
32
|
+
target.authMode === 'user' ? 'user' : 'app';
|
|
33
|
+
|
|
28
34
|
const formatPrice = (entry: RegistryEntry): string => {
|
|
29
35
|
if (entry.billingPrice == null) return 'price TBD';
|
|
30
36
|
const currency = entry.billingCurrency || 'FLC';
|
|
@@ -168,7 +174,8 @@ export function createCoreServiceGetTool(client: ForLoopAPIClient) {
|
|
|
168
174
|
`# ${args.agentId} — contract ${contract.version}`,
|
|
169
175
|
`- status: ${status}`,
|
|
170
176
|
`${sunset}${deprecated}`,
|
|
171
|
-
`- checksum: ${contract.checksum ?? 'unknown'}`,
|
|
177
|
+
`- checksum: ${contract.checksum ?? 'unknown'} (stored raw manifest)`,
|
|
178
|
+
`- generatedChecksum: ${contract.generatedChecksum ?? 'unknown'} (normalized contract; matches contract-artifact.json)`,
|
|
172
179
|
'',
|
|
173
180
|
'## OpenAPI',
|
|
174
181
|
'```json',
|
|
@@ -232,12 +239,13 @@ export function createCoreServiceIntegrationRequestTool(client: ForLoopAPIClient
|
|
|
232
239
|
].join('\n');
|
|
233
240
|
}
|
|
234
241
|
|
|
242
|
+
const subjectType = subjectTypeFor(target)
|
|
235
243
|
const resolution = await resolveSprintId(undefined, context?.directory);
|
|
236
244
|
const callerSprintId = resolution.sprintId ?? null;
|
|
237
245
|
const connectionPayload = {
|
|
238
246
|
callerSprintId,
|
|
239
247
|
targetAgentId: args.agentId,
|
|
240
|
-
subjectType
|
|
248
|
+
subjectType,
|
|
241
249
|
scopes: requested,
|
|
242
250
|
};
|
|
243
251
|
|
|
@@ -261,7 +269,7 @@ export function createCoreServiceIntegrationRequestTool(client: ForLoopAPIClient
|
|
|
261
269
|
const linkParams = new URLSearchParams();
|
|
262
270
|
linkParams.set('connect', args.agentId);
|
|
263
271
|
linkParams.set('scopes', requested.join(','));
|
|
264
|
-
linkParams.set('subject',
|
|
272
|
+
linkParams.set('subject', subjectType);
|
|
265
273
|
const fallbackConnectUrl = spaceUrl ? `${spaceUrl}?${linkParams.toString()}` : null;
|
|
266
274
|
|
|
267
275
|
// Persist the pending request and nudge the live client to open the
|
|
@@ -277,11 +285,18 @@ export function createCoreServiceIntegrationRequestTool(client: ForLoopAPIClient
|
|
|
277
285
|
sprintId: callerSprintId,
|
|
278
286
|
targetAgentId: args.agentId,
|
|
279
287
|
scopes: requested,
|
|
280
|
-
subjectType
|
|
288
|
+
subjectType,
|
|
281
289
|
requestedBy: 'planner',
|
|
282
290
|
reason: args.reason,
|
|
283
291
|
});
|
|
284
292
|
const request = created?.request ?? {};
|
|
293
|
+
if (subjectType === 'user' && request.subjectType !== 'user') {
|
|
294
|
+
return [
|
|
295
|
+
'❌ The platform did not create a user-subject connection request; nothing usable was created.',
|
|
296
|
+
`- returned subject: ${request.subjectType ?? 'unknown'}`,
|
|
297
|
+
'Do not use an app-subject request for user-owned data. Record `Connection: pending user consent (UI-only)` and stop until the delegated-user exchange is available.',
|
|
298
|
+
].join('\n');
|
|
299
|
+
}
|
|
285
300
|
requestId = typeof request.id === 'string' ? request.id : null;
|
|
286
301
|
dropped = Array.isArray(created?.droppedScopes) ? created.droppedScopes : [];
|
|
287
302
|
connectUrl = created?.link ?? request.link ?? connectUrl;
|
|
@@ -300,25 +315,29 @@ export function createCoreServiceIntegrationRequestTool(client: ForLoopAPIClient
|
|
|
300
315
|
}
|
|
301
316
|
}
|
|
302
317
|
|
|
318
|
+
const userSubject = subjectType === 'user';
|
|
319
|
+
const approver = userSubject ? 'user' : 'app owner';
|
|
303
320
|
const manualSteps = connectUrl
|
|
304
321
|
? [
|
|
305
322
|
`1. Open ${connectUrl}`,
|
|
306
|
-
` This opens **${target.name}**'s connection panel in this app, with the scopes below and
|
|
323
|
+
` This opens **${target.name}**'s connection panel in this app, with the scopes below and ${userSubject ? '`You`' : '`This app`'} preselected.`,
|
|
307
324
|
'2. Review the service, scopes and price shown above.',
|
|
308
325
|
'3. Click **Approve**.',
|
|
309
326
|
]
|
|
310
327
|
: [
|
|
311
328
|
`1. Open the app's space${spaceUrl ? `: ${spaceUrl}` : ''}.`,
|
|
312
329
|
'2. Open **Aivy Actions → App Connections (Beta)** and choose the **Connect** tab.',
|
|
313
|
-
`3. Find **${target.name}**, tick the scopes below, choose **This app
|
|
330
|
+
`3. Find **${target.name}**, tick the scopes below, choose ${userSubject ? '**You**' : '**This app**'}, then click **Approve**.`,
|
|
314
331
|
];
|
|
332
|
+
const pendingLabel = userSubject ? 'pending user consent' : 'pending owner approval';
|
|
315
333
|
|
|
316
334
|
return [
|
|
317
335
|
delivered
|
|
318
|
-
?
|
|
319
|
-
:
|
|
336
|
+
? `## App Connections opened — waiting for the ${approver} to approve`
|
|
337
|
+
: `## Approval needed — the ${approver} must approve this request`,
|
|
320
338
|
`- Service: **${target.name}** (\`${target.agentId}\`)`,
|
|
321
339
|
`- serviceCode: ${target.serviceCode ?? 'unknown'}`,
|
|
340
|
+
`- subject: ${userSubject ? 'user-subject (user consent)' : 'app-subject (app owner)'}`,
|
|
322
341
|
`- reason: ${args.reason}`,
|
|
323
342
|
`- scopes: ${requested.join(', ')}`,
|
|
324
343
|
`- cost: ${formatPrice(target)}`,
|
|
@@ -333,15 +352,17 @@ export function createCoreServiceIntegrationRequestTool(client: ForLoopAPIClient
|
|
|
333
352
|
...(delivered ? [] : ['', ...manualSteps]),
|
|
334
353
|
'',
|
|
335
354
|
'A pending request is not a grant. Do not write a connection id into the integration brief until `a2a_check_connections` reports an active connection.',
|
|
336
|
-
|
|
355
|
+
`After the ${approver} approves, call \`a2a_check_connections\` to read the real connection id and scopes.`,
|
|
337
356
|
'',
|
|
338
|
-
|
|
357
|
+
`### Connection payload (${approver} handoff)`,
|
|
339
358
|
'```json',
|
|
340
359
|
JSON.stringify(connectionPayload, null, 2),
|
|
341
360
|
'```',
|
|
342
361
|
'',
|
|
343
362
|
'The planner can never approve. Never send credentials or create the connection from the planner.',
|
|
344
|
-
|
|
363
|
+
userSubject
|
|
364
|
+
? `If the user cannot or does not consent now, write \`Connection: ${pendingLabel}\` in the integration brief and continue — do not block the plan and do not ask again. The app owner cannot consent for the user.`
|
|
365
|
+
: `If the user cannot or does not complete the approval now, write \`Connection: ${pendingLabel}\` in the integration brief and continue — do not block the plan and do not ask again.`,
|
|
345
366
|
].join('\n');
|
|
346
367
|
} catch (error: any) {
|
|
347
368
|
return `❌ Error creating integration request: ${error.message}`;
|