@pikku/core 0.12.13 → 0.12.14
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/CHANGELOG.md +16 -0
- package/dist/errors/errors.d.ts +14 -0
- package/dist/errors/errors.js +21 -0
- package/dist/function/function-runner.d.ts +1 -1
- package/dist/function/function-runner.js +8 -4
- package/dist/pikku-state.js +1 -0
- package/dist/services/credential-service.d.ts +11 -0
- package/dist/services/credential-wire-service.d.ts +14 -3
- package/dist/services/credential-wire-service.js +49 -6
- package/dist/services/local-credential-service.d.ts +2 -0
- package/dist/services/local-credential-service.js +20 -0
- package/dist/services/pikku-user-id.d.ts +3 -0
- package/dist/services/pikku-user-id.js +16 -0
- package/dist/services/typed-credential-service.d.ts +2 -0
- package/dist/services/typed-credential-service.js +6 -0
- package/dist/types/core.types.d.ts +6 -2
- package/dist/types/state.types.d.ts +7 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +21 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.js +48 -0
- package/dist/wirings/ai-agent/ai-agent-runner.js +6 -1
- package/dist/wirings/ai-agent/ai-agent-stream.d.ts +2 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +60 -3
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +20 -1
- package/dist/wirings/ai-agent/index.d.ts +1 -1
- package/dist/wirings/ai-agent/index.js +1 -1
- package/dist/wirings/cli/cli-runner.js +1 -1
- package/dist/wirings/http/http-runner.js +0 -4
- package/dist/wirings/queue/queue-runner.js +1 -0
- package/dist/wirings/queue/queue.types.d.ts +6 -0
- package/dist/wirings/rpc/rpc-runner.js +1 -0
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +2 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +2 -0
- package/dist/wirings/workflow/workflow.types.d.ts +2 -0
- package/package.json +1 -1
- package/src/errors/errors.ts +32 -0
- package/src/function/function-runner.ts +19 -9
- package/src/pikku-state.ts +1 -0
- package/src/services/credential-service.ts +13 -0
- package/src/services/credential-wire-service.test.ts +174 -0
- package/src/services/credential-wire-service.ts +52 -8
- package/src/services/local-credential-service.ts +22 -0
- package/src/services/pikku-user-id.test.ts +62 -0
- package/src/services/pikku-user-id.ts +17 -0
- package/src/services/typed-credential-service.ts +8 -0
- package/src/types/core.types.ts +8 -2
- package/src/types/state.types.ts +5 -0
- package/src/wirings/ai-agent/ai-agent-prepare.ts +71 -1
- package/src/wirings/ai-agent/ai-agent-runner.ts +6 -2
- package/src/wirings/ai-agent/ai-agent-stream.ts +107 -3
- package/src/wirings/ai-agent/ai-agent.types.ts +22 -1
- package/src/wirings/ai-agent/index.ts +1 -0
- package/src/wirings/channel/channel-handler.ts +0 -1
- package/src/wirings/cli/cli-runner.ts +1 -1
- package/src/wirings/http/http-runner.ts +0 -7
- package/src/wirings/mcp/mcp-runner.ts +0 -1
- package/src/wirings/queue/queue-runner.ts +1 -1
- package/src/wirings/queue/queue.types.ts +6 -0
- package/src/wirings/rpc/rpc-runner.ts +9 -3
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +2 -0
- package/src/wirings/workflow/pikku-workflow-service.ts +2 -0
- package/src/wirings/workflow/workflow.types.ts +2 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
## 0.12.4
|
|
2
2
|
|
|
3
|
+
## 0.12.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f85c234: Add unified credential system with per-user OAuth and AI agent pre-flight checks
|
|
8
|
+
|
|
9
|
+
- Unified CredentialService with lazy loading per user via pikkuUserId
|
|
10
|
+
- wire.getCredential() for typed single credential lookup
|
|
11
|
+
- MissingCredentialError with structured payload for client-side connect flows
|
|
12
|
+
- Console UI: Global/Users credential tabs, per-user OAuth connect/revoke
|
|
13
|
+
- AI agent pre-flight check: detects missing OAuth credentials from addon metadata, shows "Connect your accounts" prompt before chat
|
|
14
|
+
- CLI codegen: generates credentialsMeta per addon package for runtime lookup
|
|
15
|
+
- Vercel AI runner: catches MissingCredentialError as runtime fallback
|
|
16
|
+
|
|
17
|
+
- 88d3100: Fix CLI command resolution for addon functions by passing packageName to addFunction during registration.
|
|
18
|
+
|
|
3
19
|
## 0.12.13
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/dist/errors/errors.d.ts
CHANGED
|
@@ -42,6 +42,20 @@ export declare class PaymentRequiredError extends PikkuError {
|
|
|
42
42
|
*/
|
|
43
43
|
export declare class ForbiddenError extends PikkuError {
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* A required credential is missing. The payload contains metadata
|
|
47
|
+
* about the credential so the client can initiate a connect flow.
|
|
48
|
+
* @group Error
|
|
49
|
+
*/
|
|
50
|
+
export declare class MissingCredentialError extends PikkuError {
|
|
51
|
+
payload: {
|
|
52
|
+
error: 'missing_credential';
|
|
53
|
+
credentialName: string;
|
|
54
|
+
credentialType: 'oauth2' | 'apikey';
|
|
55
|
+
connectUrl?: string;
|
|
56
|
+
};
|
|
57
|
+
constructor(credentialName: string, credentialType: 'oauth2' | 'apikey', connectUrl?: string);
|
|
58
|
+
}
|
|
45
59
|
/**
|
|
46
60
|
* The session is readonly and cannot access a non-readonly function.
|
|
47
61
|
* @group Error
|
package/dist/errors/errors.js
CHANGED
|
@@ -72,6 +72,27 @@ addError(ForbiddenError, {
|
|
|
72
72
|
status: 403,
|
|
73
73
|
message: 'The client does not have permission to access the requested resource.',
|
|
74
74
|
});
|
|
75
|
+
/**
|
|
76
|
+
* A required credential is missing. The payload contains metadata
|
|
77
|
+
* about the credential so the client can initiate a connect flow.
|
|
78
|
+
* @group Error
|
|
79
|
+
*/
|
|
80
|
+
export class MissingCredentialError extends PikkuError {
|
|
81
|
+
payload;
|
|
82
|
+
constructor(credentialName, credentialType, connectUrl) {
|
|
83
|
+
super(`Missing credential: ${credentialName}`);
|
|
84
|
+
this.payload = {
|
|
85
|
+
error: 'missing_credential',
|
|
86
|
+
credentialName,
|
|
87
|
+
credentialType,
|
|
88
|
+
connectUrl,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
addError(MissingCredentialError, {
|
|
93
|
+
status: 403,
|
|
94
|
+
message: 'A required credential is not configured.',
|
|
95
|
+
});
|
|
75
96
|
/**
|
|
76
97
|
* The session is readonly and cannot access a non-readonly function.
|
|
77
98
|
* @group Error
|
|
@@ -2,7 +2,7 @@ import type { CoreServices, CoreUserSession, CorePikkuMiddleware, PikkuWiringTyp
|
|
|
2
2
|
import type { CorePikkuChannelMiddleware } from '../wirings/channel/channel.types.js';
|
|
3
3
|
import type { CorePermissionGroup, CorePikkuFunctionConfig, CorePikkuPermission } from './functions.types.js';
|
|
4
4
|
import type { SessionService } from '../services/user-session-service.js';
|
|
5
|
-
import
|
|
5
|
+
import { PikkuCredentialWireService } from '../services/credential-wire-service.js';
|
|
6
6
|
export declare const addFunction: (funcName: string, funcConfig: CorePikkuFunctionConfig<any, any>, packageName?: string | null) => void;
|
|
7
7
|
export declare const getFunctionNames: (packageName?: string | null) => string[];
|
|
8
8
|
export declare const getAllFunctionNames: () => string[];
|
|
@@ -6,7 +6,7 @@ import { coerceTopLevelDataFromSchema, validateSchema } from '../schema.js';
|
|
|
6
6
|
import { parseVersionedId } from '../version.js';
|
|
7
7
|
import { createFunctionSessionWireProps } from '../services/user-session-service.js';
|
|
8
8
|
import { ForbiddenError, ReadonlySessionError } from '../errors/errors.js';
|
|
9
|
-
import { createWireServicesCredentialWireProps } from '../services/credential-wire-service.js';
|
|
9
|
+
import { PikkuCredentialWireService, createWireServicesCredentialWireProps, } from '../services/credential-wire-service.js';
|
|
10
10
|
import { rpcService } from '../wirings/rpc/rpc-runner.js';
|
|
11
11
|
import { closeWireServices } from '../utils.js';
|
|
12
12
|
/**
|
|
@@ -114,6 +114,13 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
|
|
|
114
114
|
const resolvedWire = allChannelMiddleware.length > 0 && wire.channel
|
|
115
115
|
? wrapChannelWithMiddleware(wire, resolvedSingletonServices, allChannelMiddleware)
|
|
116
116
|
: wire;
|
|
117
|
+
// Set up credential wire service early so middleware can use setCredential.
|
|
118
|
+
// Skip if already set up (e.g. addon functions reuse the parent wire).
|
|
119
|
+
if (!resolvedWire.getCredentials) {
|
|
120
|
+
const resolvedCredentialWireService = credentialWireService ??
|
|
121
|
+
new PikkuCredentialWireService(resolvedSingletonServices.credentialService, resolvedWire);
|
|
122
|
+
Object.assign(resolvedWire, createWireServicesCredentialWireProps(resolvedCredentialWireService));
|
|
123
|
+
}
|
|
117
124
|
// Convert tags to PermissionMetadata and merge with inheritedPermissions
|
|
118
125
|
let mergedInheritedPermissions;
|
|
119
126
|
if (tags && tags.length > 0) {
|
|
@@ -189,9 +196,6 @@ export const runPikkuFunc = async (wireType, wireId, funcName, { singletonServic
|
|
|
189
196
|
packageName,
|
|
190
197
|
});
|
|
191
198
|
}
|
|
192
|
-
if (credentialWireService) {
|
|
193
|
-
Object.assign(resolvedWire, createWireServicesCredentialWireProps(credentialWireService));
|
|
194
|
-
}
|
|
195
199
|
const wireServices = await resolvedCreateWireServices?.(resolvedSingletonServices, resolvedWire);
|
|
196
200
|
try {
|
|
197
201
|
const services = wireServices && Object.keys(wireServices).length > 0
|
package/dist/pikku-state.js
CHANGED
|
@@ -37,4 +37,15 @@ export interface CredentialService {
|
|
|
37
37
|
* @returns A record of credential name to value.
|
|
38
38
|
*/
|
|
39
39
|
getAll(userId: string): Promise<Record<string, unknown>>;
|
|
40
|
+
/**
|
|
41
|
+
* Lists all user IDs that have a specific credential configured.
|
|
42
|
+
* @param name - The credential name.
|
|
43
|
+
* @returns Array of user IDs.
|
|
44
|
+
*/
|
|
45
|
+
getUsersWithCredential(name: string): Promise<string[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Lists all unique user IDs that have any credential configured.
|
|
48
|
+
* @returns Array of user IDs.
|
|
49
|
+
*/
|
|
50
|
+
getAllUsers(): Promise<string[]>;
|
|
40
51
|
}
|
|
@@ -1,13 +1,24 @@
|
|
|
1
|
+
import type { CredentialService } from './credential-service.js';
|
|
2
|
+
import type { PikkuWire } from '../types/core.types.js';
|
|
1
3
|
export declare class PikkuCredentialWireService {
|
|
4
|
+
private credentialService?;
|
|
5
|
+
private wire?;
|
|
2
6
|
private credentials;
|
|
7
|
+
private loaded;
|
|
8
|
+
private loadPromise;
|
|
9
|
+
constructor(credentialService?: CredentialService | undefined, wire?: PikkuWire | undefined);
|
|
3
10
|
set(name: string, value: unknown): void;
|
|
4
|
-
|
|
5
|
-
|
|
11
|
+
get<T = unknown>(name: string): T | null | Promise<T | null>;
|
|
12
|
+
getAll(): Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
13
|
+
getScoped(allowedNames: string[]): Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
14
|
+
private lazyLoad;
|
|
15
|
+
private doLoad;
|
|
6
16
|
}
|
|
7
17
|
export declare function createMiddlewareCredentialWireProps(credentialWire: PikkuCredentialWireService): {
|
|
8
18
|
setCredential: (name: string, value: unknown) => void;
|
|
9
19
|
};
|
|
10
20
|
export declare function createWireServicesCredentialWireProps(credentialWire: PikkuCredentialWireService, allowedNames?: string[]): {
|
|
11
21
|
setCredential: (name: string, value: unknown) => void;
|
|
12
|
-
|
|
22
|
+
getCredential: <T = unknown>(name: string) => T | Promise<T | null> | null;
|
|
23
|
+
getCredentials: () => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
13
24
|
};
|
|
@@ -1,19 +1,61 @@
|
|
|
1
|
+
import { defaultPikkuUserIdResolver } from './pikku-user-id.js';
|
|
1
2
|
export class PikkuCredentialWireService {
|
|
3
|
+
credentialService;
|
|
4
|
+
wire;
|
|
2
5
|
credentials = {};
|
|
6
|
+
loaded = false;
|
|
7
|
+
loadPromise;
|
|
8
|
+
constructor(credentialService, wire) {
|
|
9
|
+
this.credentialService = credentialService;
|
|
10
|
+
this.wire = wire;
|
|
11
|
+
}
|
|
3
12
|
set(name, value) {
|
|
4
13
|
this.credentials[name] = value;
|
|
5
14
|
}
|
|
15
|
+
get(name) {
|
|
16
|
+
if (this.loaded)
|
|
17
|
+
return this.credentials[name] ?? null;
|
|
18
|
+
return this.lazyLoad().then(() => this.credentials[name] ?? null);
|
|
19
|
+
}
|
|
6
20
|
getAll() {
|
|
7
|
-
|
|
21
|
+
if (this.loaded)
|
|
22
|
+
return this.credentials;
|
|
23
|
+
return this.lazyLoad().then(() => this.credentials);
|
|
8
24
|
}
|
|
9
25
|
getScoped(allowedNames) {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
26
|
+
const buildScoped = () => {
|
|
27
|
+
const scoped = {};
|
|
28
|
+
for (const name of allowedNames) {
|
|
29
|
+
if (name in this.credentials) {
|
|
30
|
+
scoped[name] = this.credentials[name];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return scoped;
|
|
34
|
+
};
|
|
35
|
+
if (this.loaded)
|
|
36
|
+
return buildScoped();
|
|
37
|
+
return this.lazyLoad().then(buildScoped);
|
|
38
|
+
}
|
|
39
|
+
lazyLoad() {
|
|
40
|
+
if (this.loadPromise)
|
|
41
|
+
return this.loadPromise;
|
|
42
|
+
this.loadPromise = this.doLoad();
|
|
43
|
+
return this.loadPromise;
|
|
44
|
+
}
|
|
45
|
+
async doLoad() {
|
|
46
|
+
this.loaded = true;
|
|
47
|
+
if (!this.credentialService || !this.wire)
|
|
48
|
+
return;
|
|
49
|
+
const userId = defaultPikkuUserIdResolver(this.wire);
|
|
50
|
+
if (!userId)
|
|
51
|
+
return;
|
|
52
|
+
this.wire.pikkuUserId = userId;
|
|
53
|
+
const allCreds = await this.credentialService.getAll(userId);
|
|
54
|
+
for (const [name, value] of Object.entries(allCreds)) {
|
|
55
|
+
if (!(name in this.credentials)) {
|
|
56
|
+
this.credentials[name] = value;
|
|
14
57
|
}
|
|
15
58
|
}
|
|
16
|
-
return scoped;
|
|
17
59
|
}
|
|
18
60
|
}
|
|
19
61
|
export function createMiddlewareCredentialWireProps(credentialWire) {
|
|
@@ -24,6 +66,7 @@ export function createMiddlewareCredentialWireProps(credentialWire) {
|
|
|
24
66
|
export function createWireServicesCredentialWireProps(credentialWire, allowedNames) {
|
|
25
67
|
return {
|
|
26
68
|
setCredential: (name, value) => credentialWire.set(name, value),
|
|
69
|
+
getCredential: (name) => credentialWire.get(name),
|
|
27
70
|
getCredentials: () => allowedNames
|
|
28
71
|
? credentialWire.getScoped(allowedNames)
|
|
29
72
|
: credentialWire.getAll(),
|
|
@@ -7,4 +7,6 @@ export declare class LocalCredentialService implements CredentialService {
|
|
|
7
7
|
delete(name: string, userId?: string): Promise<void>;
|
|
8
8
|
has(name: string, userId?: string): Promise<boolean>;
|
|
9
9
|
getAll(userId: string): Promise<Record<string, unknown>>;
|
|
10
|
+
getUsersWithCredential(name: string): Promise<string[]>;
|
|
11
|
+
getAllUsers(): Promise<string[]>;
|
|
10
12
|
}
|
|
@@ -30,4 +30,24 @@ export class LocalCredentialService {
|
|
|
30
30
|
}
|
|
31
31
|
return result;
|
|
32
32
|
}
|
|
33
|
+
async getUsersWithCredential(name) {
|
|
34
|
+
const suffix = `:${name}`;
|
|
35
|
+
const users = [];
|
|
36
|
+
for (const key of this.store.keys()) {
|
|
37
|
+
if (key.endsWith(suffix)) {
|
|
38
|
+
users.push(key.slice(0, -suffix.length));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return users;
|
|
42
|
+
}
|
|
43
|
+
async getAllUsers() {
|
|
44
|
+
const users = new Set();
|
|
45
|
+
for (const key of this.store.keys()) {
|
|
46
|
+
const colonIndex = key.indexOf(':');
|
|
47
|
+
if (colonIndex > 0) {
|
|
48
|
+
users.add(key.slice(0, colonIndex));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return [...users];
|
|
52
|
+
}
|
|
33
53
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const defaultPikkuUserIdResolver = (wire) => {
|
|
2
|
+
// Explicit pikkuUserId on wire (set by earlier middleware or runner)
|
|
3
|
+
if (wire.pikkuUserId)
|
|
4
|
+
return wire.pikkuUserId;
|
|
5
|
+
// Session userId (from auth middleware — user-defined session shape)
|
|
6
|
+
const session = wire.session;
|
|
7
|
+
if (session?.userId && typeof session.userId === 'string')
|
|
8
|
+
return session.userId;
|
|
9
|
+
// Queue job: carried in job metadata
|
|
10
|
+
if (wire.queue?.pikkuUserId)
|
|
11
|
+
return wire.queue.pikkuUserId;
|
|
12
|
+
// Workflow: carried in run wire metadata
|
|
13
|
+
if (wire.workflow?.pikkuUserId)
|
|
14
|
+
return wire.workflow.pikkuUserId;
|
|
15
|
+
return undefined;
|
|
16
|
+
};
|
|
@@ -22,6 +22,8 @@ export declare class TypedCredentialService<TMap = Record<string, unknown>> impl
|
|
|
22
22
|
delete(name: string, userId?: string): Promise<void>;
|
|
23
23
|
has(name: string, userId?: string): Promise<boolean>;
|
|
24
24
|
getAll(userId: string): Promise<Record<string, unknown>>;
|
|
25
|
+
getUsersWithCredential(name: string): Promise<string[]>;
|
|
26
|
+
getAllUsers(): Promise<string[]>;
|
|
25
27
|
getAllStatus(userId?: string): Promise<CredentialStatusInfo[]>;
|
|
26
28
|
getMissing(userId?: string): Promise<CredentialStatusInfo[]>;
|
|
27
29
|
}
|
|
@@ -20,6 +20,12 @@ export class TypedCredentialService {
|
|
|
20
20
|
async getAll(userId) {
|
|
21
21
|
return this.credentials.getAll(userId);
|
|
22
22
|
}
|
|
23
|
+
async getUsersWithCredential(name) {
|
|
24
|
+
return this.credentials.getUsersWithCredential(name);
|
|
25
|
+
}
|
|
26
|
+
async getAllUsers() {
|
|
27
|
+
return this.credentials.getAllUsers();
|
|
28
|
+
}
|
|
23
29
|
async getAllStatus(userId) {
|
|
24
30
|
const results = [];
|
|
25
31
|
for (const [name, meta] of Object.entries(this.credentialsMeta)) {
|
|
@@ -203,10 +203,14 @@ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boo
|
|
|
203
203
|
getSession: () => Promise<UserSession> | UserSession | undefined;
|
|
204
204
|
/** Whether the session was modified during this run */
|
|
205
205
|
hasSessionChanged: () => boolean;
|
|
206
|
+
/** The resolved user identity for credential lookups */
|
|
207
|
+
pikkuUserId: string;
|
|
206
208
|
/** Set a credential value (available in middleware) */
|
|
207
209
|
setCredential: (name: string, value: unknown) => void;
|
|
208
|
-
/** Get
|
|
209
|
-
|
|
210
|
+
/** Get a single credential by name — lazy-loads from CredentialService on first call, sync thereafter */
|
|
211
|
+
getCredential: <T = unknown>(name: string) => T | null | Promise<T | null>;
|
|
212
|
+
/** Get all resolved credentials — lazy-loads from CredentialService on first call, sync thereafter */
|
|
213
|
+
getCredentials: () => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
210
214
|
}>;
|
|
211
215
|
/**
|
|
212
216
|
* A function that can wrap an wire and be called before or after
|
|
@@ -132,5 +132,12 @@ export interface PikkuPackageState {
|
|
|
132
132
|
singletonServices: CoreSingletonServices | null;
|
|
133
133
|
/** Absolute path to this package's .pikku directory */
|
|
134
134
|
metaDir: string | null;
|
|
135
|
+
/** Credential metadata for this addon package */
|
|
136
|
+
credentialsMeta: Record<string, {
|
|
137
|
+
name: string;
|
|
138
|
+
displayName: string;
|
|
139
|
+
type: string;
|
|
140
|
+
oauth2?: boolean;
|
|
141
|
+
}> | null;
|
|
135
142
|
};
|
|
136
143
|
}
|
|
@@ -19,6 +19,27 @@ export declare class ToolApprovalRequired extends PikkuError {
|
|
|
19
19
|
readonly agentRunId?: string;
|
|
20
20
|
constructor(toolCallId: string, toolName: string, args: unknown, reason?: string, displayToolName?: string, displayArgs?: unknown, agentRunId?: string);
|
|
21
21
|
}
|
|
22
|
+
export declare class ToolCredentialRequired extends PikkuError {
|
|
23
|
+
readonly toolCallId: string;
|
|
24
|
+
readonly toolName: string;
|
|
25
|
+
readonly args: unknown;
|
|
26
|
+
readonly credentialName: string;
|
|
27
|
+
readonly credentialType: 'oauth2' | 'apikey';
|
|
28
|
+
readonly connectUrl?: string;
|
|
29
|
+
constructor(toolCallId: string, toolName: string, args: unknown, credentialName: string, credentialType: 'oauth2' | 'apikey', connectUrl?: string);
|
|
30
|
+
}
|
|
31
|
+
export interface AddonCredentialRequirement {
|
|
32
|
+
credentialName: string;
|
|
33
|
+
displayName: string;
|
|
34
|
+
addonNamespace: string;
|
|
35
|
+
type: 'wire';
|
|
36
|
+
oauth2: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Given a list of tool names (e.g. ["oauth-api:getProfile"]),
|
|
40
|
+
* returns the wire OAuth credentials required by their addons.
|
|
41
|
+
*/
|
|
42
|
+
export declare function getAddonCredentialRequirements(toolNames: string[]): AddonCredentialRequirement[];
|
|
22
43
|
export type StreamContext = {
|
|
23
44
|
channel: AIStreamChannel;
|
|
24
45
|
options?: StreamAIAgentOptions;
|
|
@@ -28,6 +28,52 @@ export class ToolApprovalRequired extends PikkuError {
|
|
|
28
28
|
this.agentRunId = agentRunId;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
export class ToolCredentialRequired extends PikkuError {
|
|
32
|
+
toolCallId;
|
|
33
|
+
toolName;
|
|
34
|
+
args;
|
|
35
|
+
credentialName;
|
|
36
|
+
credentialType;
|
|
37
|
+
connectUrl;
|
|
38
|
+
constructor(toolCallId, toolName, args, credentialName, credentialType, connectUrl) {
|
|
39
|
+
super(`Tool '${toolName}' requires credential '${credentialName}'`);
|
|
40
|
+
this.toolCallId = toolCallId;
|
|
41
|
+
this.toolName = toolName;
|
|
42
|
+
this.args = args;
|
|
43
|
+
this.credentialName = credentialName;
|
|
44
|
+
this.credentialType = credentialType;
|
|
45
|
+
this.connectUrl = connectUrl;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Given a list of tool names (e.g. ["oauth-api:getProfile"]),
|
|
50
|
+
* returns the wire OAuth credentials required by their addons.
|
|
51
|
+
*/
|
|
52
|
+
export function getAddonCredentialRequirements(toolNames) {
|
|
53
|
+
const requirements = new Map();
|
|
54
|
+
for (const toolName of toolNames) {
|
|
55
|
+
if (!toolName.includes(':'))
|
|
56
|
+
continue;
|
|
57
|
+
const resolved = resolveNamespace(toolName);
|
|
58
|
+
if (!resolved)
|
|
59
|
+
continue;
|
|
60
|
+
const credsMeta = pikkuState(resolved.package, 'package', 'credentialsMeta');
|
|
61
|
+
if (!credsMeta)
|
|
62
|
+
continue;
|
|
63
|
+
for (const [name, meta] of Object.entries(credsMeta)) {
|
|
64
|
+
if (meta.type === 'wire' && meta.oauth2 && !requirements.has(name)) {
|
|
65
|
+
requirements.set(name, {
|
|
66
|
+
credentialName: name,
|
|
67
|
+
displayName: meta.displayName ?? name,
|
|
68
|
+
addonNamespace: toolName.split(':')[0],
|
|
69
|
+
type: 'wire',
|
|
70
|
+
oauth2: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return [...requirements.values()];
|
|
76
|
+
}
|
|
31
77
|
export const resolveAgent = (agentName) => {
|
|
32
78
|
const mainAgent = pikkuState(null, 'agent', 'agents').get(agentName);
|
|
33
79
|
if (mainAgent) {
|
|
@@ -324,6 +370,8 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
|
|
|
324
370
|
}
|
|
325
371
|
catch (err) {
|
|
326
372
|
execError = err;
|
|
373
|
+
if (err?.payload?.error === 'missing_credential')
|
|
374
|
+
throw err;
|
|
327
375
|
result = err instanceof Error ? err.message : String(err);
|
|
328
376
|
}
|
|
329
377
|
const durationMs = Date.now() - startTime;
|
|
@@ -298,7 +298,12 @@ export async function resumeAIAgentSync(runId, approvals, params, expectedAgentN
|
|
|
298
298
|
: JSON.stringify(toolResult);
|
|
299
299
|
}
|
|
300
300
|
catch (err) {
|
|
301
|
-
|
|
301
|
+
if (err?.payload?.error === 'missing_credential') {
|
|
302
|
+
resultStr = JSON.stringify(err.payload);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
resultStr = `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
306
|
+
}
|
|
302
307
|
}
|
|
303
308
|
}
|
|
304
309
|
else {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { AIStreamChannel } from './ai-agent.types.js';
|
|
2
2
|
import type { AIAgentRunnerParams, AIAgentStepResult } from '../../services/ai-agent-runner-service.js';
|
|
3
|
-
import { ToolApprovalRequired, type RunAIAgentParams, type StreamAIAgentOptions } from './ai-agent-prepare.js';
|
|
3
|
+
import { ToolApprovalRequired, ToolCredentialRequired, type RunAIAgentParams, type StreamAIAgentOptions } from './ai-agent-prepare.js';
|
|
4
4
|
export declare function checkForApprovals(stepResult: AIAgentStepResult, tools: AIAgentRunnerParams['tools'], runId: string): ToolApprovalRequired[];
|
|
5
|
+
export declare function checkForCredentialRequests(stepResult: AIAgentStepResult, runId: string): ToolCredentialRequired[];
|
|
5
6
|
export declare function appendStepMessages(runnerParams: AIAgentRunnerParams, stepResult: AIAgentStepResult): void;
|
|
6
7
|
export declare function streamAIAgent(agentName: string, input: {
|
|
7
8
|
message: string;
|
|
@@ -3,7 +3,7 @@ import { AIProviderNotConfiguredError } from '../../errors/errors.js';
|
|
|
3
3
|
import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../channel/channel-middleware-runner.js';
|
|
4
4
|
import { randomUUID } from 'crypto';
|
|
5
5
|
import { parseWorkingMemory, deepMergeWorkingMemory, resolveMemoryServices, loadContextMessages, trimMessages, } from './ai-agent-memory.js';
|
|
6
|
-
import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, ToolApprovalRequired, } from './ai-agent-prepare.js';
|
|
6
|
+
import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, ToolApprovalRequired, ToolCredentialRequired, } from './ai-agent-prepare.js';
|
|
7
7
|
import { resolveModelConfig } from './ai-agent-model-config.js';
|
|
8
8
|
function createPersistingChannel(parent, storage, threadId) {
|
|
9
9
|
let fullText = '';
|
|
@@ -197,6 +197,12 @@ async function runStreamStepLoop(params) {
|
|
|
197
197
|
}
|
|
198
198
|
return { outcome: 'approval', approvals: approvalsNeeded };
|
|
199
199
|
}
|
|
200
|
+
const credentialRequests = checkForCredentialRequests(stepResult, runId);
|
|
201
|
+
if (credentialRequests.length > 0) {
|
|
202
|
+
// Append step messages so the tool result is preserved for resume
|
|
203
|
+
appendStepMessages(runnerParams, stepResult);
|
|
204
|
+
return { outcome: 'credential', credentialRequests };
|
|
205
|
+
}
|
|
200
206
|
appendStepMessages(runnerParams, stepResult);
|
|
201
207
|
}
|
|
202
208
|
return { outcome: 'done' };
|
|
@@ -226,6 +232,19 @@ export function checkForApprovals(stepResult, tools, runId) {
|
|
|
226
232
|
}
|
|
227
233
|
return approvals;
|
|
228
234
|
}
|
|
235
|
+
export function checkForCredentialRequests(stepResult, runId) {
|
|
236
|
+
const requests = [];
|
|
237
|
+
for (const tr of stepResult.toolResults) {
|
|
238
|
+
if (tr.result &&
|
|
239
|
+
typeof tr.result === 'object' &&
|
|
240
|
+
'__credentialRequired' in tr.result) {
|
|
241
|
+
const r = tr.result;
|
|
242
|
+
const tc = stepResult.toolCalls.find((t) => t.toolCallId === tr.toolCallId);
|
|
243
|
+
requests.push(new ToolCredentialRequired(tr.toolCallId, tc?.toolName ?? 'unknown', tc?.args ?? {}, r.credentialName, r.credentialType, r.connectUrl));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return requests;
|
|
247
|
+
}
|
|
229
248
|
export function appendStepMessages(runnerParams, stepResult) {
|
|
230
249
|
const assistantMsg = {
|
|
231
250
|
id: randomUUID(),
|
|
@@ -293,6 +312,30 @@ function handleApprovals(approvals, runId, channel, aiRunState, persistingChanne
|
|
|
293
312
|
channel.close();
|
|
294
313
|
})();
|
|
295
314
|
}
|
|
315
|
+
function handleCredentialRequests(requests, runId, channel, aiRunState, persistingChannel) {
|
|
316
|
+
return (async () => {
|
|
317
|
+
await persistingChannel.flush();
|
|
318
|
+
const pendingApprovals = requests.map((req) => ({
|
|
319
|
+
type: 'credential-request',
|
|
320
|
+
toolCallId: req.toolCallId,
|
|
321
|
+
toolName: req.toolName,
|
|
322
|
+
args: req.args,
|
|
323
|
+
credentialName: req.credentialName,
|
|
324
|
+
credentialType: req.credentialType,
|
|
325
|
+
connectUrl: req.connectUrl,
|
|
326
|
+
}));
|
|
327
|
+
await aiRunState.updateRun(runId, {
|
|
328
|
+
status: 'suspended',
|
|
329
|
+
suspendReason: 'credential',
|
|
330
|
+
pendingApprovals,
|
|
331
|
+
});
|
|
332
|
+
// Don't send credential-request SSE events — the tool result with
|
|
333
|
+
// __credentialRequired was already streamed. The frontend detects it
|
|
334
|
+
// from the tool result and shows Connect/Ignore buttons.
|
|
335
|
+
channel.send({ type: 'done' });
|
|
336
|
+
channel.close();
|
|
337
|
+
})();
|
|
338
|
+
}
|
|
296
339
|
export async function streamAIAgent(agentName, input, channel, params, agentSessionMap, options) {
|
|
297
340
|
const sessionMap = agentSessionMap ?? new Map();
|
|
298
341
|
const normalizedInput = input;
|
|
@@ -419,6 +462,10 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
|
|
|
419
462
|
await handleApprovals(loopResult.approvals, runId, channel, aiRunState, persistingChannel);
|
|
420
463
|
return persistingChannel.fullText;
|
|
421
464
|
}
|
|
465
|
+
if (loopResult.outcome === 'credential') {
|
|
466
|
+
await handleCredentialRequests(loopResult.credentialRequests, runId, channel, aiRunState, persistingChannel);
|
|
467
|
+
return persistingChannel.fullText;
|
|
468
|
+
}
|
|
422
469
|
await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, storage, memoryConfig, threadId, workingMemorySchemaName, runnerParams.messages, aiRunState, runId);
|
|
423
470
|
channel.send({ type: 'done' });
|
|
424
471
|
channel.close();
|
|
@@ -487,7 +534,8 @@ export async function resumeAIAgent(input, channel, params, options) {
|
|
|
487
534
|
toolResults: [
|
|
488
535
|
{
|
|
489
536
|
id: input.toolCallId,
|
|
490
|
-
name: pending.type === 'tool-call'
|
|
537
|
+
name: pending.type === 'tool-call' ||
|
|
538
|
+
pending.type === 'credential-request'
|
|
491
539
|
? pending.toolName
|
|
492
540
|
: pending.agentName,
|
|
493
541
|
result: denialResult,
|
|
@@ -573,7 +621,12 @@ export async function resumeAIAgent(input, channel, params, options) {
|
|
|
573
621
|
toolResult = await matchingTool.execute(toolArgs);
|
|
574
622
|
}
|
|
575
623
|
catch (execErr) {
|
|
576
|
-
|
|
624
|
+
if (execErr?.payload?.error === 'missing_credential') {
|
|
625
|
+
toolResult = execErr.payload;
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
toolResult = `Error: ${execErr instanceof Error ? execErr.message : String(execErr)}`;
|
|
629
|
+
}
|
|
577
630
|
isError = true;
|
|
578
631
|
}
|
|
579
632
|
const resultStr = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult);
|
|
@@ -708,6 +761,10 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
|
|
|
708
761
|
await handleApprovals(loopResult.approvals, run.runId, channel, aiRunState, persistingChannel);
|
|
709
762
|
return;
|
|
710
763
|
}
|
|
764
|
+
if (loopResult.outcome === 'credential') {
|
|
765
|
+
await handleCredentialRequests(loopResult.credentialRequests, run.runId, channel, aiRunState, persistingChannel);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
711
768
|
await postStreamCleanup(persistingChannel, aiMiddlewares, singletonServices, storage, memoryConfig, run.threadId, workingMemorySchemaName, runnerParams.messages, aiRunState, run.runId);
|
|
712
769
|
channel.send({ type: 'done' });
|
|
713
770
|
channel.close();
|
|
@@ -256,6 +256,17 @@ export type AIStreamEvent = {
|
|
|
256
256
|
reason?: string;
|
|
257
257
|
agent?: string;
|
|
258
258
|
session?: string;
|
|
259
|
+
} | {
|
|
260
|
+
type: 'credential-request';
|
|
261
|
+
toolCallId: string;
|
|
262
|
+
toolName: string;
|
|
263
|
+
args: unknown;
|
|
264
|
+
credentialName: string;
|
|
265
|
+
credentialType: 'oauth2' | 'apikey';
|
|
266
|
+
connectUrl?: string;
|
|
267
|
+
runId: string;
|
|
268
|
+
agent?: string;
|
|
269
|
+
session?: string;
|
|
259
270
|
} | {
|
|
260
271
|
type: 'usage';
|
|
261
272
|
tokens: {
|
|
@@ -307,6 +318,14 @@ export type PendingApproval = {
|
|
|
307
318
|
agentRunId: string;
|
|
308
319
|
displayToolName: string;
|
|
309
320
|
displayArgs: unknown;
|
|
321
|
+
} | {
|
|
322
|
+
type: 'credential-request';
|
|
323
|
+
toolCallId: string;
|
|
324
|
+
toolName: string;
|
|
325
|
+
args: unknown;
|
|
326
|
+
credentialName: string;
|
|
327
|
+
credentialType: 'oauth2' | 'apikey';
|
|
328
|
+
connectUrl?: string;
|
|
310
329
|
};
|
|
311
330
|
export interface AgentRunState {
|
|
312
331
|
runId: string;
|
|
@@ -315,7 +334,7 @@ export interface AgentRunState {
|
|
|
315
334
|
resourceId: string;
|
|
316
335
|
status: 'running' | 'suspended' | 'completed' | 'failed';
|
|
317
336
|
errorMessage?: string;
|
|
318
|
-
suspendReason?: 'approval' | 'rpc-missing';
|
|
337
|
+
suspendReason?: 'approval' | 'credential' | 'rpc-missing';
|
|
319
338
|
missingRpcs?: string[];
|
|
320
339
|
pendingApprovals?: PendingApproval[];
|
|
321
340
|
usage: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { agent, agentStream, agentResume, agentApprove, } from './ai-agent-helpers.js';
|
|
2
2
|
export { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
|
|
3
3
|
export { streamAIAgent, resumeAIAgent } from './ai-agent-stream.js';
|
|
4
|
-
export { type RunAIAgentParams, type StreamAIAgentOptions, ToolApprovalRequired, } from './ai-agent-prepare.js';
|
|
4
|
+
export { type RunAIAgentParams, type StreamAIAgentOptions, ToolApprovalRequired, ToolCredentialRequired, } from './ai-agent-prepare.js';
|
|
5
5
|
export { addAIAgent, approveAIAgent, getAIAgents, getAIAgentsMeta, } from './ai-agent-registry.js';
|
|
6
6
|
export type { AIAgentInput, AIAgentInputAttachment, AIAgentMeta, AIAgentMemoryConfig, AIAgentStep, AIContentPart, AgentRunRow, AgentRunService, AgentRunState, AIMessage, AIStreamChannel, AIStreamEvent, AIThread, CoreAIAgent, PendingApproval, PikkuAIMiddlewareHooks, } from './ai-agent.types.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { agent, agentStream, agentResume, agentApprove, } from './ai-agent-helpers.js';
|
|
2
2
|
export { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
|
|
3
3
|
export { streamAIAgent, resumeAIAgent } from './ai-agent-stream.js';
|
|
4
|
-
export { ToolApprovalRequired, } from './ai-agent-prepare.js';
|
|
4
|
+
export { ToolApprovalRequired, ToolCredentialRequired, } from './ai-agent-prepare.js';
|
|
5
5
|
export { addAIAgent, approveAIAgent, getAIAgents, getAIAgentsMeta, } from './ai-agent-registry.js';
|