@pikku/core 0.12.22 → 0.12.24
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 +22 -0
- package/dist/function/function-runner.js +62 -15
- package/dist/function/functions.types.d.ts +8 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/middleware-runner.d.ts +2 -2
- package/dist/permissions.d.ts +1 -1
- package/dist/services/ai-agent-runner-service.d.ts +6 -0
- package/dist/services/audit-service.d.ts +50 -0
- package/dist/services/audit-service.js +106 -0
- package/dist/services/content-service.d.ts +1 -0
- package/dist/services/email-service.d.ts +36 -0
- package/dist/services/email-service.js +1 -0
- package/dist/services/index.d.ts +5 -1
- package/dist/services/index.js +2 -0
- package/dist/services/local-content.js +1 -0
- package/dist/services/local-email-service.d.ts +4 -0
- package/dist/services/local-email-service.js +26 -0
- package/dist/services/local-secrets.d.ts +1 -0
- package/dist/services/local-secrets.js +11 -2
- package/dist/services/meta-service.d.ts +36 -0
- package/dist/services/meta-service.js +59 -0
- package/dist/types/core.types.d.ts +15 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.d.ts +2 -0
- package/dist/wirings/ai-agent/ai-agent-prepare.js +8 -2
- package/dist/wirings/ai-agent/ai-agent-runner.js +3 -1
- package/dist/wirings/ai-agent/ai-agent-stream.js +2 -1
- package/dist/wirings/ai-agent/ai-agent-utils.d.ts +1 -0
- package/dist/wirings/ai-agent/ai-agent-utils.js +1 -0
- package/dist/wirings/rpc/rpc-runner.js +9 -2
- package/package.json +1 -1
- package/src/function/function-runner.test.ts +255 -0
- package/src/function/function-runner.ts +66 -20
- package/src/function/functions.types.ts +26 -11
- package/src/index.ts +35 -0
- package/src/middleware-runner.ts +10 -10
- package/src/permissions.ts +4 -4
- package/src/services/ai-agent-runner-service.ts +6 -0
- package/src/services/audit-service.test.ts +44 -0
- package/src/services/audit-service.ts +198 -0
- package/src/services/content-service.ts +1 -0
- package/src/services/email-service.ts +46 -0
- package/src/services/index.ts +33 -0
- package/src/services/local-content.ts +1 -0
- package/src/services/local-email-service.test.ts +108 -0
- package/src/services/local-email-service.ts +30 -0
- package/src/services/local-secrets.ts +11 -2
- package/src/services/meta-service.ts +115 -0
- package/src/types/core.types.ts +20 -2
- package/src/wirings/ai-agent/ai-agent-prepare.ts +11 -2
- package/src/wirings/ai-agent/ai-agent-runner.test.ts +105 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +4 -1
- package/src/wirings/ai-agent/ai-agent-stream.ts +3 -1
- package/src/wirings/ai-agent/ai-agent-utils.ts +1 -0
- package/src/wirings/rpc/rpc-runner.ts +9 -2
- package/tsconfig.tsbuildinfo +1 -1
package/src/services/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
} from './credential-wire-service.js'
|
|
18
18
|
export { TypedVariablesService } from './typed-variables-service.js'
|
|
19
19
|
export { LocalSecretService } from './local-secrets.js'
|
|
20
|
+
export { LocalEmailService } from './local-email-service.js'
|
|
20
21
|
export { LocalCredentialService } from './local-credential-service.js'
|
|
21
22
|
export { LocalVariablesService } from './local-variables.js'
|
|
22
23
|
export { ConsoleLogger, JsonConsoleLogger } from './logger-console.js'
|
|
@@ -36,6 +37,15 @@ export type {
|
|
|
36
37
|
CopyFileArgs,
|
|
37
38
|
} from './content-service.js'
|
|
38
39
|
export type { JWTService } from './jwt-service.js'
|
|
40
|
+
export type {
|
|
41
|
+
EmailService,
|
|
42
|
+
EmailTemplateReference,
|
|
43
|
+
SendEmailInput,
|
|
44
|
+
SendEmailResult,
|
|
45
|
+
SendHTMLEmailInput,
|
|
46
|
+
SendTemplateEmailInput,
|
|
47
|
+
SendTextEmailInput,
|
|
48
|
+
} from './email-service.js'
|
|
39
49
|
export type { Logger } from './logger.js'
|
|
40
50
|
export type { SecretService } from './secret-service.js'
|
|
41
51
|
export type { VariablesService } from './variables-service.js'
|
|
@@ -78,6 +88,25 @@ export type {
|
|
|
78
88
|
export type { VariableStatus, VariableMeta } from './typed-variables-service.js'
|
|
79
89
|
export type { MetaService } from './meta-service.js'
|
|
80
90
|
export type { SessionStore } from './session-store.js'
|
|
91
|
+
export {
|
|
92
|
+
NoopAuditService,
|
|
93
|
+
createInvocationAudit,
|
|
94
|
+
resolveAuditActorFromWire,
|
|
95
|
+
resolveAuditConfig,
|
|
96
|
+
} from './audit-service.js'
|
|
97
|
+
export type {
|
|
98
|
+
AuditActor,
|
|
99
|
+
AuditConfig,
|
|
100
|
+
AuditDurability,
|
|
101
|
+
AuditEvent,
|
|
102
|
+
AuditEventBatch,
|
|
103
|
+
AuditLog,
|
|
104
|
+
AuditLogWriteInput,
|
|
105
|
+
AuditOutcome,
|
|
106
|
+
AuditService,
|
|
107
|
+
AuditSource,
|
|
108
|
+
ResolvedAuditConfig,
|
|
109
|
+
} from './audit-service.js'
|
|
81
110
|
export { InMemorySessionStore } from './in-memory-session-store.js'
|
|
82
111
|
export type {
|
|
83
112
|
MCPMeta,
|
|
@@ -96,4 +125,8 @@ export type {
|
|
|
96
125
|
PermissionMeta,
|
|
97
126
|
AgentsMeta,
|
|
98
127
|
AgentMeta,
|
|
128
|
+
EmailsMeta,
|
|
129
|
+
EmailTemplateMeta,
|
|
130
|
+
EmailTemplateLocaleMeta,
|
|
131
|
+
EmailTemplateAssets,
|
|
99
132
|
} from './meta-service.js'
|
|
@@ -106,6 +106,7 @@ export class LocalContent implements ContentService {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
public async getUploadURL(args: GetUploadURLArgs): Promise<UploadURLResult> {
|
|
109
|
+
void args.visibility
|
|
109
110
|
const fullKey = this.joinKey(args.bucket, args.fileKey)
|
|
110
111
|
this.logger.debug(`Going to upload with key: ${fullKey}`)
|
|
111
112
|
return {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { LocalEmailService } from './local-email-service.js'
|
|
5
|
+
|
|
6
|
+
test('LocalEmailService logs sent email metadata as JSON', async () => {
|
|
7
|
+
const service = new LocalEmailService()
|
|
8
|
+
const writes: string[] = []
|
|
9
|
+
const originalInfo = console.info
|
|
10
|
+
console.info = (value?: unknown) => {
|
|
11
|
+
writes.push(String(value))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
await service.send({
|
|
16
|
+
to: 'user@example.com',
|
|
17
|
+
from: 'hello@example.com',
|
|
18
|
+
subject: 'Welcome',
|
|
19
|
+
template: {
|
|
20
|
+
name: 'welcome',
|
|
21
|
+
locale: 'en',
|
|
22
|
+
data: { userName: 'Yasser' },
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
} finally {
|
|
26
|
+
console.info = originalInfo
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
assert.equal(writes.length, 1)
|
|
30
|
+
const payload = JSON.parse(writes[0])
|
|
31
|
+
assert.equal(payload.type, 'email')
|
|
32
|
+
assert.equal(payload.message, 'this email was sent')
|
|
33
|
+
assert.equal(payload.to, 'user@example.com')
|
|
34
|
+
assert.equal(payload.from, 'hello@example.com')
|
|
35
|
+
assert.equal(payload.subject, 'Welcome')
|
|
36
|
+
assert.deepEqual(payload.template, {
|
|
37
|
+
name: 'welcome',
|
|
38
|
+
locale: 'en',
|
|
39
|
+
data: { userName: 'Yasser' },
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('LocalEmailService logs htmlLength and textLength for HTML email', async () => {
|
|
44
|
+
const service = new LocalEmailService()
|
|
45
|
+
const writes: string[] = []
|
|
46
|
+
const originalInfo = console.info
|
|
47
|
+
console.info = (value?: unknown) => {
|
|
48
|
+
writes.push(String(value))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await service.send({
|
|
53
|
+
to: 'user@example.com',
|
|
54
|
+
html: '<p>Hello</p>',
|
|
55
|
+
text: 'Hello',
|
|
56
|
+
})
|
|
57
|
+
} finally {
|
|
58
|
+
console.info = originalInfo
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const payload = JSON.parse(writes[0])
|
|
62
|
+
assert.equal(payload.htmlLength, '<p>Hello</p>'.length)
|
|
63
|
+
assert.equal(payload.textLength, 'Hello'.length)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('LocalEmailService logs textLength only for plain text email', async () => {
|
|
67
|
+
const service = new LocalEmailService()
|
|
68
|
+
const writes: string[] = []
|
|
69
|
+
const originalInfo = console.info
|
|
70
|
+
console.info = (value?: unknown) => {
|
|
71
|
+
writes.push(String(value))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await service.send({
|
|
76
|
+
to: 'user@example.com',
|
|
77
|
+
text: 'Plain text body',
|
|
78
|
+
})
|
|
79
|
+
} finally {
|
|
80
|
+
console.info = originalInfo
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const payload = JSON.parse(writes[0])
|
|
84
|
+
assert.equal(payload.textLength, 'Plain text body'.length)
|
|
85
|
+
assert.equal(payload.htmlLength, undefined)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('LocalEmailService logs null textLength for HTML email without plain text', async () => {
|
|
89
|
+
const service = new LocalEmailService()
|
|
90
|
+
const writes: string[] = []
|
|
91
|
+
const originalInfo = console.info
|
|
92
|
+
console.info = (value?: unknown) => {
|
|
93
|
+
writes.push(String(value))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await service.send({
|
|
98
|
+
to: 'user@example.com',
|
|
99
|
+
html: '<p>Hello</p>',
|
|
100
|
+
})
|
|
101
|
+
} finally {
|
|
102
|
+
console.info = originalInfo
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const payload = JSON.parse(writes[0])
|
|
106
|
+
assert.equal(payload.htmlLength, '<p>Hello</p>'.length)
|
|
107
|
+
assert.equal(payload.textLength, null)
|
|
108
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { EmailService, SendEmailInput, SendEmailResult } from './email-service.js'
|
|
2
|
+
|
|
3
|
+
export class LocalEmailService implements EmailService {
|
|
4
|
+
async send(input: SendEmailInput): Promise<SendEmailResult> {
|
|
5
|
+
const payload: Record<string, unknown> = {
|
|
6
|
+
type: 'email',
|
|
7
|
+
message: 'this email was sent',
|
|
8
|
+
to: input.to,
|
|
9
|
+
from: input.from ?? null,
|
|
10
|
+
cc: input.cc ?? null,
|
|
11
|
+
bcc: input.bcc ?? null,
|
|
12
|
+
replyTo: input.replyTo ?? null,
|
|
13
|
+
subject: input.subject ?? null,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if ('template' in input && input.template) {
|
|
17
|
+
payload.template = input.template
|
|
18
|
+
}
|
|
19
|
+
if ('html' in input && typeof input.html === 'string') {
|
|
20
|
+
payload.htmlLength = input.html.length
|
|
21
|
+
payload.textLength = input.text?.length ?? null
|
|
22
|
+
}
|
|
23
|
+
if ('text' in input && typeof input.text === 'string') {
|
|
24
|
+
payload.textLength = input.text.length
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.info(JSON.stringify(payload))
|
|
28
|
+
return {}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -9,6 +9,15 @@ import type { VariablesService } from './variables-service.js'
|
|
|
9
9
|
export class LocalSecretService implements SecretService {
|
|
10
10
|
private localSecrets: Map<string, string> = new Map()
|
|
11
11
|
|
|
12
|
+
// TODO: Drop this fallback once secrets and variables expose aligned typed/raw access paths again.
|
|
13
|
+
private parseSecret<R>(raw: string): R {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(raw) as R
|
|
16
|
+
} catch {
|
|
17
|
+
return raw as unknown as R
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
/**
|
|
13
22
|
* Creates an instance of LocalSecretService.
|
|
14
23
|
*/
|
|
@@ -27,13 +36,13 @@ export class LocalSecretService implements SecretService {
|
|
|
27
36
|
// Check local storage first
|
|
28
37
|
const localValue = this.localSecrets.get(key)
|
|
29
38
|
if (localValue) {
|
|
30
|
-
return
|
|
39
|
+
return this.parseSecret(localValue)
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
// Fall back to environment variables
|
|
34
43
|
const value = await this.variables.get(key)
|
|
35
44
|
if (value) {
|
|
36
|
-
return
|
|
45
|
+
return this.parseSecret(value)
|
|
37
46
|
}
|
|
38
47
|
throw new Error('Requested secret not found')
|
|
39
48
|
}
|
|
@@ -109,6 +109,38 @@ export interface PermissionsGroupsMeta {
|
|
|
109
109
|
tagGroups: Record<string, GroupMeta>
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
export interface EmailTemplateLocaleMeta {
|
|
113
|
+
contentHash: string
|
|
114
|
+
htmlHash: string
|
|
115
|
+
subjectHash: string
|
|
116
|
+
textHash: string
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface EmailTemplateMeta {
|
|
120
|
+
variables: string[]
|
|
121
|
+
hasHtml: boolean
|
|
122
|
+
hasSubject: boolean
|
|
123
|
+
hasText: boolean
|
|
124
|
+
locales: Record<string, EmailTemplateLocaleMeta>
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface EmailsMeta {
|
|
128
|
+
src: string
|
|
129
|
+
themeHash: string
|
|
130
|
+
templates: Record<string, EmailTemplateMeta>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface EmailTemplateAssets {
|
|
134
|
+
theme: Record<string, unknown>
|
|
135
|
+
strings: Record<string, unknown>
|
|
136
|
+
layout: string
|
|
137
|
+
partials: Record<string, string>
|
|
138
|
+
html: string
|
|
139
|
+
subject: string
|
|
140
|
+
text: string
|
|
141
|
+
missing: string[]
|
|
142
|
+
}
|
|
143
|
+
|
|
112
144
|
/**
|
|
113
145
|
* Abstraction over .pikku metadata file access.
|
|
114
146
|
* All paths are relative to the .pikku root directory.
|
|
@@ -123,6 +155,8 @@ export interface MetaService {
|
|
|
123
155
|
readFile(relativePath: string): Promise<string | null>
|
|
124
156
|
/** List files in a directory by relative path. Returns empty array if not found. */
|
|
125
157
|
readDir(relativePath: string): Promise<string[]>
|
|
158
|
+
/** Read a project source file relative to the project root (one level above .pikku). */
|
|
159
|
+
readProjectFile(relativePath: string): Promise<string | null>
|
|
126
160
|
|
|
127
161
|
// -- Typed metadata accessors --
|
|
128
162
|
|
|
@@ -143,6 +177,11 @@ export interface MetaService {
|
|
|
143
177
|
getSecretsMeta(): Promise<SecretDefinitionsMeta>
|
|
144
178
|
getCredentialsMeta(): Promise<CredentialDefinitionsMeta>
|
|
145
179
|
getVariablesMeta(): Promise<VariableDefinitionsMeta>
|
|
180
|
+
getEmailMeta(): Promise<EmailsMeta>
|
|
181
|
+
getEmailTemplateAssets(
|
|
182
|
+
templateName: string,
|
|
183
|
+
locale: string
|
|
184
|
+
): Promise<EmailTemplateAssets>
|
|
146
185
|
getServicesMeta(): Promise<ServicesMetaRecord>
|
|
147
186
|
|
|
148
187
|
getSchema(schemaName: string): Promise<JSONSchema7 | null>
|
|
@@ -173,6 +212,7 @@ export class LocalMetaService implements MetaService {
|
|
|
173
212
|
private secretsMetaCache: SecretDefinitionsMeta | null = null
|
|
174
213
|
private credentialsMetaCache: CredentialDefinitionsMeta | null = null
|
|
175
214
|
private variablesMetaCache: VariableDefinitionsMeta | null = null
|
|
215
|
+
private emailMetaCache: EmailsMeta | null = null
|
|
176
216
|
private middlewareGroupsMetaCache: MiddlewareGroupsMeta | null = null
|
|
177
217
|
private permissionsGroupsMetaCache: PermissionsGroupsMeta | null = null
|
|
178
218
|
private agentsMetaCache: AgentsMeta | null = null
|
|
@@ -198,6 +238,14 @@ export class LocalMetaService implements MetaService {
|
|
|
198
238
|
}
|
|
199
239
|
}
|
|
200
240
|
|
|
241
|
+
async readProjectFile(relativePath: string): Promise<string | null> {
|
|
242
|
+
try {
|
|
243
|
+
return await readFile(join(this.basePath, '..', relativePath), 'utf-8')
|
|
244
|
+
} catch {
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
201
249
|
clearCache(): void {
|
|
202
250
|
this.httpMetaCache = null
|
|
203
251
|
this.channelsMetaCache = null
|
|
@@ -214,6 +262,7 @@ export class LocalMetaService implements MetaService {
|
|
|
214
262
|
this.secretsMetaCache = null
|
|
215
263
|
this.credentialsMetaCache = null
|
|
216
264
|
this.variablesMetaCache = null
|
|
265
|
+
this.emailMetaCache = null
|
|
217
266
|
this.middlewareGroupsMetaCache = null
|
|
218
267
|
this.permissionsGroupsMetaCache = null
|
|
219
268
|
this.agentsMetaCache = null
|
|
@@ -459,6 +508,72 @@ export class LocalMetaService implements MetaService {
|
|
|
459
508
|
return this.variablesMetaCache!
|
|
460
509
|
}
|
|
461
510
|
|
|
511
|
+
async getEmailMeta(): Promise<EmailsMeta> {
|
|
512
|
+
if (this.emailMetaCache) return this.emailMetaCache
|
|
513
|
+
|
|
514
|
+
const content = await this.readFile('email/pikku-emails-meta.gen.json')
|
|
515
|
+
this.emailMetaCache = content
|
|
516
|
+
? JSON.parse(content)
|
|
517
|
+
: {
|
|
518
|
+
src: '',
|
|
519
|
+
themeHash: '',
|
|
520
|
+
templates: {},
|
|
521
|
+
}
|
|
522
|
+
return this.emailMetaCache!
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async getEmailTemplateAssets(
|
|
526
|
+
templateName: string,
|
|
527
|
+
locale: string
|
|
528
|
+
): Promise<EmailTemplateAssets> {
|
|
529
|
+
const emailsMeta = await this.getEmailMeta()
|
|
530
|
+
if (!emailsMeta.src) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
'No generated email metadata found. Run `pikku emails generate`.'
|
|
533
|
+
)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const baseDir = emailsMeta.src
|
|
537
|
+
const [
|
|
538
|
+
themeRaw,
|
|
539
|
+
localeRaw,
|
|
540
|
+
layoutRaw,
|
|
541
|
+
footerRaw,
|
|
542
|
+
templateHtml,
|
|
543
|
+
templateSubject,
|
|
544
|
+
templateText,
|
|
545
|
+
] = await Promise.all([
|
|
546
|
+
this.readProjectFile(`${baseDir}/theme.json`),
|
|
547
|
+
this.readProjectFile(`${baseDir}/locales/${locale}.json`),
|
|
548
|
+
this.readProjectFile(`${baseDir}/partials/layout.html`),
|
|
549
|
+
this.readProjectFile(`${baseDir}/partials/footer.html`),
|
|
550
|
+
this.readProjectFile(`${baseDir}/templates/${templateName}.html`),
|
|
551
|
+
this.readProjectFile(`${baseDir}/templates/${templateName}.subject.txt`),
|
|
552
|
+
this.readProjectFile(`${baseDir}/templates/${templateName}.text.txt`),
|
|
553
|
+
])
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
theme: themeRaw ? (JSON.parse(themeRaw) as Record<string, unknown>) : {},
|
|
557
|
+
strings: localeRaw
|
|
558
|
+
? (JSON.parse(localeRaw) as Record<string, unknown>)
|
|
559
|
+
: {},
|
|
560
|
+
layout: layoutRaw ?? '',
|
|
561
|
+
partials: {
|
|
562
|
+
footer: footerRaw ?? '',
|
|
563
|
+
},
|
|
564
|
+
html: templateHtml ?? '',
|
|
565
|
+
subject: templateSubject ?? '',
|
|
566
|
+
text: templateText ?? '',
|
|
567
|
+
missing: [
|
|
568
|
+
...(themeRaw ? [] : ['theme']),
|
|
569
|
+
...(localeRaw ? [] : ['locale']),
|
|
570
|
+
...(templateHtml ? [] : ['html']),
|
|
571
|
+
...(templateSubject ? [] : ['subject']),
|
|
572
|
+
...(templateText ? [] : ['text']),
|
|
573
|
+
],
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
462
577
|
async getServicesMeta(): Promise<ServicesMetaRecord> {
|
|
463
578
|
if (this.servicesMetaCache) return this.servicesMetaCache
|
|
464
579
|
|
package/src/types/core.types.ts
CHANGED
|
@@ -35,8 +35,13 @@ import type { AgentRunService } from '../wirings/ai-agent/ai-agent.types.js'
|
|
|
35
35
|
import type { PikkuAIMiddlewareHooks } from '../wirings/ai-agent/ai-agent.types.js'
|
|
36
36
|
import type { WorkflowRunService } from '../wirings/workflow/workflow.types.js'
|
|
37
37
|
import type { CredentialService } from '../services/credential-service.js'
|
|
38
|
+
import type { EmailService } from '../services/email-service.js'
|
|
38
39
|
import type { MetaService } from '../services/meta-service.js'
|
|
39
40
|
import type { SessionStore } from '../services/session-store.js'
|
|
41
|
+
import type {
|
|
42
|
+
AuditDurability,
|
|
43
|
+
AuditService,
|
|
44
|
+
} from '../services/audit-service.js'
|
|
40
45
|
|
|
41
46
|
export type PikkuWiringTypes =
|
|
42
47
|
| 'http'
|
|
@@ -196,7 +201,10 @@ export type CoreConfig<Config extends Record<string, unknown> = {}> = {
|
|
|
196
201
|
/**
|
|
197
202
|
* Represents a core user session, which can be extended for more specific session information.
|
|
198
203
|
*/
|
|
199
|
-
export interface CoreUserSession {
|
|
204
|
+
export interface CoreUserSession {
|
|
205
|
+
userId?: string
|
|
206
|
+
orgId?: string
|
|
207
|
+
}
|
|
200
208
|
|
|
201
209
|
/**
|
|
202
210
|
* Interface for core singleton services provided by Pikku.
|
|
@@ -239,8 +247,12 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
|
|
|
239
247
|
workflowRunService?: WorkflowRunService
|
|
240
248
|
/** Credential service for dynamic/managed credentials (OAuth tokens, per-user API keys) */
|
|
241
249
|
credentialService?: CredentialService
|
|
250
|
+
/** Email service for outbound messages and template-backed delivery */
|
|
251
|
+
emailService?: EmailService
|
|
242
252
|
/** Meta service for reading .pikku metadata files (filesystem on Node, R2/KV on CF) */
|
|
243
253
|
metaService?: MetaService
|
|
254
|
+
/** Audit service for durable or staged audit event capture */
|
|
255
|
+
audit?: AuditService
|
|
244
256
|
/** Session store for persisting user sessions keyed by pikkuUserId */
|
|
245
257
|
sessionStore?: SessionStore
|
|
246
258
|
}
|
|
@@ -263,6 +275,8 @@ export type PikkuWire<
|
|
|
263
275
|
wireId: string
|
|
264
276
|
/** Trace ID for distributed tracing — propagated across remote RPC calls via x-trace-id header */
|
|
265
277
|
traceId: string
|
|
278
|
+
/** Function id for the current invocation */
|
|
279
|
+
functionId: string
|
|
266
280
|
http: PikkuHTTP<In>
|
|
267
281
|
mcp: PikkuMCP<MCPTools>
|
|
268
282
|
rpc: TypedRPC
|
|
@@ -281,7 +295,7 @@ export type PikkuWire<
|
|
|
281
295
|
? UserSession
|
|
282
296
|
: UserSession | undefined
|
|
283
297
|
/** Update and persist the current session */
|
|
284
|
-
setSession: (session:
|
|
298
|
+
setSession: (session: UserSession) => Promise<void> | void
|
|
285
299
|
/** Clear and persist the current session */
|
|
286
300
|
clearSession: () => Promise<void> | void
|
|
287
301
|
/** Fetch the latest session (may read from backing store) */
|
|
@@ -298,6 +312,10 @@ export type PikkuWire<
|
|
|
298
312
|
getCredentials: () =>
|
|
299
313
|
| Record<string, unknown>
|
|
300
314
|
| Promise<Record<string, unknown>>
|
|
315
|
+
/** Resolved function audit metadata for this invocation, if enabled */
|
|
316
|
+
audit: {
|
|
317
|
+
durability: AuditDurability
|
|
318
|
+
}
|
|
301
319
|
}>
|
|
302
320
|
|
|
303
321
|
/**
|
|
@@ -16,7 +16,7 @@ import { AIProviderNotConfiguredError } from '../../errors/errors.js'
|
|
|
16
16
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js'
|
|
17
17
|
import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js'
|
|
18
18
|
import type { SessionService } from '../../services/user-session-service.js'
|
|
19
|
-
import { randomUUID } from '
|
|
19
|
+
import { randomUUID } from './ai-agent-utils.js'
|
|
20
20
|
import { streamAIAgent } from './ai-agent-stream.js'
|
|
21
21
|
import { runAIAgent } from './ai-agent-runner.js'
|
|
22
22
|
import {
|
|
@@ -32,6 +32,8 @@ import { resolveModelConfig } from './ai-agent-model-config.js'
|
|
|
32
32
|
|
|
33
33
|
export type RunAIAgentParams = {
|
|
34
34
|
sessionService?: SessionService<CoreUserSession>
|
|
35
|
+
/** Credential accessor for the current request — used to read per-user overrides (e.g. AI_API_KEY). */
|
|
36
|
+
getCredential?: <T = unknown>(name: string) => T | null | Promise<T | null>
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export type StreamAIAgentOptions = {
|
|
@@ -616,11 +618,18 @@ export async function prepareAgentRun(
|
|
|
616
618
|
const singletonServices = getSingletonServices()
|
|
617
619
|
const { agent, packageName, resolvedName } = resolveAgent(agentName)
|
|
618
620
|
|
|
619
|
-
|
|
621
|
+
let agentRunner = singletonServices.aiAgentRunner
|
|
620
622
|
if (!agentRunner) {
|
|
621
623
|
throw new AIProviderNotConfiguredError()
|
|
622
624
|
}
|
|
623
625
|
|
|
626
|
+
if (params.getCredential && agentRunner.withApiKey) {
|
|
627
|
+
const aiCredential = await params.getCredential<{ apiKey: string }>('AI_API_KEY')
|
|
628
|
+
if (aiCredential?.apiKey?.trim()) {
|
|
629
|
+
agentRunner = agentRunner.withApiKey(aiCredential.apiKey)
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
624
633
|
const { storage } = resolveMemoryServices(agent, singletonServices)
|
|
625
634
|
const memoryConfig = agent.memory
|
|
626
635
|
const threadId = input.threadId
|
|
@@ -970,3 +970,108 @@ describe('resumeAIAgentSync', () => {
|
|
|
970
970
|
assert.equal(result.text, 'Recovered')
|
|
971
971
|
})
|
|
972
972
|
})
|
|
973
|
+
|
|
974
|
+
describe('getCredential API key override', () => {
|
|
975
|
+
test('uses withApiKey runner when getCredential returns apiKey', async () => {
|
|
976
|
+
addTestAgent('api-key-agent')
|
|
977
|
+
|
|
978
|
+
const originalRunCalls: string[] = []
|
|
979
|
+
const overrideRunCalls: string[] = []
|
|
980
|
+
|
|
981
|
+
const overrideRunner = {
|
|
982
|
+
run: async (): Promise<AIAgentStepResult> => {
|
|
983
|
+
overrideRunCalls.push('override')
|
|
984
|
+
return makeStepResult({ text: 'from-override', finishReason: 'stop' })
|
|
985
|
+
},
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const mockServices = {
|
|
989
|
+
logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
|
|
990
|
+
aiAgentRunner: {
|
|
991
|
+
run: async (): Promise<AIAgentStepResult> => {
|
|
992
|
+
originalRunCalls.push('original')
|
|
993
|
+
return makeStepResult({ text: 'from-original', finishReason: 'stop' })
|
|
994
|
+
},
|
|
995
|
+
withApiKey: (_key: string) => overrideRunner,
|
|
996
|
+
},
|
|
997
|
+
aiRunState: {
|
|
998
|
+
createRun: async () => 'run-key-override',
|
|
999
|
+
updateRun: async () => {},
|
|
1000
|
+
},
|
|
1001
|
+
} as any
|
|
1002
|
+
|
|
1003
|
+
pikkuState(null, 'package', 'singletonServices', mockServices)
|
|
1004
|
+
|
|
1005
|
+
const result = await runAIAgent(
|
|
1006
|
+
'api-key-agent',
|
|
1007
|
+
{ message: 'hi', threadId: 't-key', resourceId: 'r-key' },
|
|
1008
|
+
{ getCredential: async () => ({ apiKey: 'my-secret-key' }) }
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
assert.equal(result.text, 'from-override')
|
|
1012
|
+
assert.equal(overrideRunCalls.length, 1)
|
|
1013
|
+
assert.equal(originalRunCalls.length, 0)
|
|
1014
|
+
})
|
|
1015
|
+
|
|
1016
|
+
test('uses original runner when getCredential returns null', async () => {
|
|
1017
|
+
addTestAgent('no-cred-agent')
|
|
1018
|
+
|
|
1019
|
+
const originalRunCalls: string[] = []
|
|
1020
|
+
|
|
1021
|
+
const mockServices = {
|
|
1022
|
+
logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
|
|
1023
|
+
aiAgentRunner: {
|
|
1024
|
+
run: async (): Promise<AIAgentStepResult> => {
|
|
1025
|
+
originalRunCalls.push('original')
|
|
1026
|
+
return makeStepResult({ text: 'from-original', finishReason: 'stop' })
|
|
1027
|
+
},
|
|
1028
|
+
withApiKey: (_key: string) => ({ run: async () => makeStepResult({ text: 'should-not-be-called', finishReason: 'stop' }) }),
|
|
1029
|
+
},
|
|
1030
|
+
aiRunState: {
|
|
1031
|
+
createRun: async () => 'run-no-cred',
|
|
1032
|
+
updateRun: async () => {},
|
|
1033
|
+
},
|
|
1034
|
+
} as any
|
|
1035
|
+
|
|
1036
|
+
pikkuState(null, 'package', 'singletonServices', mockServices)
|
|
1037
|
+
|
|
1038
|
+
const result = await runAIAgent(
|
|
1039
|
+
'no-cred-agent',
|
|
1040
|
+
{ message: 'hi', threadId: 't-no-cred', resourceId: 'r-no-cred' },
|
|
1041
|
+
{ getCredential: async () => null }
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
assert.equal(result.text, 'from-original')
|
|
1045
|
+
assert.equal(originalRunCalls.length, 1)
|
|
1046
|
+
})
|
|
1047
|
+
|
|
1048
|
+
test('uses original runner when runner does not implement withApiKey', async () => {
|
|
1049
|
+
addTestAgent('no-withkey-agent')
|
|
1050
|
+
|
|
1051
|
+
const originalRunCalls: string[] = []
|
|
1052
|
+
|
|
1053
|
+
const mockServices = {
|
|
1054
|
+
logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
|
|
1055
|
+
aiAgentRunner: {
|
|
1056
|
+
run: async (): Promise<AIAgentStepResult> => {
|
|
1057
|
+
originalRunCalls.push('original')
|
|
1058
|
+
return makeStepResult({ text: 'from-original', finishReason: 'stop' })
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
aiRunState: {
|
|
1062
|
+
createRun: async () => 'run-no-withkey',
|
|
1063
|
+
updateRun: async () => {},
|
|
1064
|
+
},
|
|
1065
|
+
} as any
|
|
1066
|
+
|
|
1067
|
+
pikkuState(null, 'package', 'singletonServices', mockServices)
|
|
1068
|
+
|
|
1069
|
+
await runAIAgent(
|
|
1070
|
+
'no-withkey-agent',
|
|
1071
|
+
{ message: 'hi', threadId: 't-no-withkey', resourceId: 'r-no-withkey' },
|
|
1072
|
+
{ getCredential: async () => ({ apiKey: 'ignored-key' }) }
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
assert.equal(originalRunCalls.length, 1)
|
|
1076
|
+
})
|
|
1077
|
+
})
|
|
@@ -33,6 +33,7 @@ import { checkForApprovals, appendStepMessages } from './ai-agent-stream.js'
|
|
|
33
33
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js'
|
|
34
34
|
import { resolveModelConfig } from './ai-agent-model-config.js'
|
|
35
35
|
import { AIProviderNotConfiguredError } from '../../errors/errors.js'
|
|
36
|
+
import { randomUUID } from './ai-agent-utils.js'
|
|
36
37
|
|
|
37
38
|
function stripNulls(obj: unknown): unknown {
|
|
38
39
|
if (obj === null) return undefined
|
|
@@ -46,7 +47,6 @@ function stripNulls(obj: unknown): unknown {
|
|
|
46
47
|
}
|
|
47
48
|
return result
|
|
48
49
|
}
|
|
49
|
-
import { randomUUID } from 'crypto'
|
|
50
50
|
|
|
51
51
|
function extractStructuredAssistantOutput(object: unknown): {
|
|
52
52
|
text: string | null
|
|
@@ -84,6 +84,8 @@ export async function runAIAgent(
|
|
|
84
84
|
workingMemorySchemaName,
|
|
85
85
|
} = await prepareAgentRun(agentName, input, params, sessionMap)
|
|
86
86
|
|
|
87
|
+
runnerParams.agentId = agentName
|
|
88
|
+
|
|
87
89
|
const singletonServices = getSingletonServices()
|
|
88
90
|
const { aiRunState } = singletonServices
|
|
89
91
|
if (!aiRunState) {
|
|
@@ -597,6 +599,7 @@ async function continueAfterToolResultSync(
|
|
|
597
599
|
outputSchema: meta?.outputSchema
|
|
598
600
|
? pikkuState(packageName, 'misc', 'schemas').get(meta.outputSchema)
|
|
599
601
|
: undefined,
|
|
602
|
+
agentId: run.agentName,
|
|
600
603
|
}
|
|
601
604
|
|
|
602
605
|
try {
|
|
@@ -11,11 +11,11 @@ import type {
|
|
|
11
11
|
} from './ai-agent.types.js'
|
|
12
12
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js'
|
|
13
13
|
import { AIProviderNotConfiguredError } from '../../errors/errors.js'
|
|
14
|
+
import { randomUUID } from './ai-agent-utils.js'
|
|
14
15
|
import {
|
|
15
16
|
combineChannelMiddleware,
|
|
16
17
|
wrapChannelWithMiddleware,
|
|
17
18
|
} from '../channel/channel-middleware-runner.js'
|
|
18
|
-
import { randomUUID } from 'crypto'
|
|
19
19
|
import type { AIStorageService } from '../../services/ai-storage-service.js'
|
|
20
20
|
import type {
|
|
21
21
|
AIAgentRunnerParams,
|
|
@@ -595,6 +595,8 @@ export async function streamAIAgent(
|
|
|
595
595
|
streamContext
|
|
596
596
|
)
|
|
597
597
|
|
|
598
|
+
runnerParams.agentId = agentName
|
|
599
|
+
|
|
598
600
|
const singletonServices = getSingletonServices()
|
|
599
601
|
const { aiRunState } = singletonServices
|
|
600
602
|
if (!aiRunState) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const randomUUID = (): string => globalThis.crypto.randomUUID()
|