@pikku/kysely 0.12.1 → 0.12.3
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 +26 -0
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +1 -1
- package/dist/src/kysely-agent-run-service.js +2 -0
- package/dist/src/kysely-ai-storage-service.d.ts +1 -0
- package/dist/src/kysely-ai-storage-service.js +75 -45
- package/dist/src/kysely-deployment-service.d.ts +2 -1
- package/dist/src/kysely-deployment-service.js +18 -6
- package/dist/src/kysely-secret-service.d.ts +29 -0
- package/dist/src/kysely-secret-service.js +142 -0
- package/dist/src/kysely-tables.d.ts +27 -8
- package/dist/src/kysely-workflow-run-service.d.ts +5 -0
- package/dist/src/kysely-workflow-run-service.js +16 -0
- package/dist/src/kysely-workflow-service.d.ts +11 -5
- package/dist/src/kysely-workflow-service.js +18 -22
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -6
- package/src/index.ts +2 -2
- package/src/kysely-agent-run-service.ts +2 -0
- package/src/kysely-ai-storage-service.ts +96 -55
- package/src/kysely-deployment-service.ts +28 -13
- package/src/kysely-secret-service.ts +197 -0
- package/src/kysely-services.test.ts +89 -698
- package/src/kysely-tables.ts +33 -8
- package/src/kysely-workflow-run-service.ts +20 -1
- package/src/kysely-workflow-service.ts +30 -24
- package/dist/src/pikku-kysely.d.ts +0 -13
- package/dist/src/pikku-kysely.js +0 -58
- package/src/pikku-kysely.ts +0 -67
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
## 0.12.0
|
|
2
2
|
|
|
3
|
+
## 0.12.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 32ed003: Add envelope encryption utilities and database-backed secret services with KEK rotation support
|
|
8
|
+
- 387b2ee: Add error_message column to agent run storage and queries
|
|
9
|
+
- b2b0af9: Migrate all consumers from @pikku/pg to @pikku/kysely and remove the @pikku/pg package
|
|
10
|
+
- c7ff141: Add WorkflowVersionStatus type with draft→active lifecycle for AI-generated workflows, type all DB status fields with proper unions instead of plain strings
|
|
11
|
+
- Updated dependencies [387b2ee]
|
|
12
|
+
- Updated dependencies [32ed003]
|
|
13
|
+
- Updated dependencies [7d369f3]
|
|
14
|
+
- Updated dependencies [508a796]
|
|
15
|
+
- Updated dependencies [ffe83af]
|
|
16
|
+
- Updated dependencies [c7ff141]
|
|
17
|
+
- @pikku/core@0.12.3
|
|
18
|
+
|
|
19
|
+
## 0.12.2
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- ce961b5: fix: improve MySQL compatibility in AI storage service by using varchar columns with explicit lengths instead of text for primary keys, foreign keys, and indexed columns, and handle duplicate index errors gracefully
|
|
24
|
+
- 3e04565: chore: update dependencies to latest minor/patch versions
|
|
25
|
+
- Updated dependencies [cc4c9e9]
|
|
26
|
+
- Updated dependencies [3e04565]
|
|
27
|
+
- @pikku/core@0.12.2
|
|
28
|
+
|
|
3
29
|
## 0.12.1
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export { PikkuKysely } from './pikku-kysely.js';
|
|
2
1
|
export { KyselyChannelStore } from './kysely-channel-store.js';
|
|
3
2
|
export { KyselyEventHubStore } from './kysely-eventhub-store.js';
|
|
4
3
|
export { KyselyWorkflowService } from './kysely-workflow-service.js';
|
|
@@ -6,6 +5,8 @@ export { KyselyWorkflowRunService } from './kysely-workflow-run-service.js';
|
|
|
6
5
|
export { KyselyDeploymentService } from './kysely-deployment-service.js';
|
|
7
6
|
export { KyselyAIStorageService } from './kysely-ai-storage-service.js';
|
|
8
7
|
export { KyselyAgentRunService } from './kysely-agent-run-service.js';
|
|
8
|
+
export { KyselySecretService } from './kysely-secret-service.js';
|
|
9
|
+
export type { KyselySecretServiceConfig } from './kysely-secret-service.js';
|
|
9
10
|
export type { KyselyPikkuDB } from './kysely-tables.js';
|
|
10
11
|
export type { WorkflowRunService } from '@pikku/core/workflow';
|
|
11
12
|
export type { AgentRunService, AgentRunRow } from '@pikku/core/ai-agent';
|
package/dist/src/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export { PikkuKysely } from './pikku-kysely.js';
|
|
2
1
|
export { KyselyChannelStore } from './kysely-channel-store.js';
|
|
3
2
|
export { KyselyEventHubStore } from './kysely-eventhub-store.js';
|
|
4
3
|
export { KyselyWorkflowService } from './kysely-workflow-service.js';
|
|
@@ -6,3 +5,4 @@ export { KyselyWorkflowRunService } from './kysely-workflow-run-service.js';
|
|
|
6
5
|
export { KyselyDeploymentService } from './kysely-deployment-service.js';
|
|
7
6
|
export { KyselyAIStorageService } from './kysely-ai-storage-service.js';
|
|
8
7
|
export { KyselyAgentRunService } from './kysely-agent-run-service.js';
|
|
8
|
+
export { KyselySecretService } from './kysely-secret-service.js';
|
|
@@ -113,6 +113,7 @@ export class KyselyAgentRunService {
|
|
|
113
113
|
'thread_id',
|
|
114
114
|
'resource_id',
|
|
115
115
|
'status',
|
|
116
|
+
'error_message',
|
|
116
117
|
'suspend_reason',
|
|
117
118
|
'missing_rpcs',
|
|
118
119
|
'usage_input_tokens',
|
|
@@ -159,6 +160,7 @@ export class KyselyAgentRunService {
|
|
|
159
160
|
threadId: row.thread_id,
|
|
160
161
|
resourceId: row.resource_id,
|
|
161
162
|
status: row.status,
|
|
163
|
+
errorMessage: row.error_message ?? undefined,
|
|
162
164
|
suspendReason: row.suspend_reason ?? undefined,
|
|
163
165
|
missingRpcs: parseJson(row.missing_rpcs),
|
|
164
166
|
usageInputTokens: Number(row.usage_input_tokens),
|
|
@@ -6,6 +6,7 @@ export declare class KyselyAIStorageService implements AIStorageService, AIRunSt
|
|
|
6
6
|
private db;
|
|
7
7
|
private initialized;
|
|
8
8
|
constructor(db: Kysely<KyselyPikkuDB>);
|
|
9
|
+
private createIndexSafe;
|
|
9
10
|
init(): Promise<void>;
|
|
10
11
|
createThread(resourceId: string, options?: {
|
|
11
12
|
threadId?: string;
|
|
@@ -6,6 +6,22 @@ export class KyselyAIStorageService {
|
|
|
6
6
|
constructor(db) {
|
|
7
7
|
this.db = db;
|
|
8
8
|
}
|
|
9
|
+
async createIndexSafe(builder) {
|
|
10
|
+
try {
|
|
11
|
+
await builder.execute();
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
// Ignore "index already exists" errors across databases
|
|
15
|
+
// MySQL: ER_DUP_KEYNAME, Postgres: 42P07, SQLite: "already exists"
|
|
16
|
+
if (e?.code === 'ER_DUP_KEYNAME' || e?.errno === 1061)
|
|
17
|
+
return;
|
|
18
|
+
if (e?.code === '42P07')
|
|
19
|
+
return;
|
|
20
|
+
if (e?.message?.includes('already exists'))
|
|
21
|
+
return;
|
|
22
|
+
throw e;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
9
25
|
async init() {
|
|
10
26
|
if (this.initialized) {
|
|
11
27
|
return;
|
|
@@ -13,68 +29,60 @@ export class KyselyAIStorageService {
|
|
|
13
29
|
await this.db.schema
|
|
14
30
|
.createTable('ai_threads')
|
|
15
31
|
.ifNotExists()
|
|
16
|
-
.addColumn('id', '
|
|
17
|
-
.addColumn('resource_id', '
|
|
32
|
+
.addColumn('id', 'varchar(36)', (col) => col.primaryKey())
|
|
33
|
+
.addColumn('resource_id', 'varchar(255)', (col) => col.notNull())
|
|
18
34
|
.addColumn('title', 'text')
|
|
19
35
|
.addColumn('metadata', 'text')
|
|
20
36
|
.addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
21
37
|
.addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
22
38
|
.execute();
|
|
23
|
-
await this.db.schema
|
|
39
|
+
await this.createIndexSafe(this.db.schema
|
|
24
40
|
.createIndex('idx_ai_threads_resource')
|
|
25
|
-
.ifNotExists()
|
|
26
41
|
.on('ai_threads')
|
|
27
|
-
.column('resource_id')
|
|
28
|
-
.execute();
|
|
42
|
+
.column('resource_id'));
|
|
29
43
|
await this.db.schema
|
|
30
44
|
.createTable('ai_message')
|
|
31
45
|
.ifNotExists()
|
|
32
|
-
.addColumn('id', '
|
|
33
|
-
.addColumn('thread_id', '
|
|
34
|
-
.addColumn('role', '
|
|
46
|
+
.addColumn('id', 'varchar(36)', (col) => col.primaryKey())
|
|
47
|
+
.addColumn('thread_id', 'varchar(36)', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
|
|
48
|
+
.addColumn('role', 'varchar(50)', (col) => col.notNull())
|
|
35
49
|
.addColumn('content', 'text')
|
|
36
50
|
.addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
37
51
|
.execute();
|
|
38
|
-
await this.db.schema
|
|
52
|
+
await this.createIndexSafe(this.db.schema
|
|
39
53
|
.createIndex('idx_ai_message_thread')
|
|
40
|
-
.ifNotExists()
|
|
41
54
|
.on('ai_message')
|
|
42
|
-
.columns(['thread_id', 'created_at'])
|
|
43
|
-
.execute();
|
|
55
|
+
.columns(['thread_id', 'created_at']));
|
|
44
56
|
await this.db.schema
|
|
45
57
|
.createTable('ai_tool_call')
|
|
46
58
|
.ifNotExists()
|
|
47
|
-
.addColumn('id', '
|
|
48
|
-
.addColumn('thread_id', '
|
|
49
|
-
.addColumn('message_id', '
|
|
50
|
-
.addColumn('run_id', '
|
|
51
|
-
.addColumn('tool_name', '
|
|
52
|
-
.addColumn('args', 'text', (col) => col.notNull()
|
|
59
|
+
.addColumn('id', 'varchar(36)', (col) => col.primaryKey())
|
|
60
|
+
.addColumn('thread_id', 'varchar(36)', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
|
|
61
|
+
.addColumn('message_id', 'varchar(36)', (col) => col.notNull().references('ai_message.id').onDelete('cascade'))
|
|
62
|
+
.addColumn('run_id', 'varchar(36)')
|
|
63
|
+
.addColumn('tool_name', 'varchar(255)', (col) => col.notNull())
|
|
64
|
+
.addColumn('args', 'text', (col) => col.notNull())
|
|
53
65
|
.addColumn('result', 'text')
|
|
54
|
-
.addColumn('approval_status', '
|
|
55
|
-
.addColumn('approval_type', '
|
|
56
|
-
.addColumn('agent_run_id', '
|
|
57
|
-
.addColumn('display_tool_name', '
|
|
66
|
+
.addColumn('approval_status', 'varchar(50)')
|
|
67
|
+
.addColumn('approval_type', 'varchar(50)')
|
|
68
|
+
.addColumn('agent_run_id', 'varchar(36)')
|
|
69
|
+
.addColumn('display_tool_name', 'varchar(255)')
|
|
58
70
|
.addColumn('display_args', 'text')
|
|
59
71
|
.addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
60
72
|
.execute();
|
|
61
|
-
await this.db.schema
|
|
73
|
+
await this.createIndexSafe(this.db.schema
|
|
62
74
|
.createIndex('idx_ai_tool_call_thread')
|
|
63
|
-
.ifNotExists()
|
|
64
75
|
.on('ai_tool_call')
|
|
65
|
-
.column('thread_id')
|
|
66
|
-
|
|
67
|
-
await this.db.schema
|
|
76
|
+
.column('thread_id'));
|
|
77
|
+
await this.createIndexSafe(this.db.schema
|
|
68
78
|
.createIndex('idx_ai_tool_call_message')
|
|
69
|
-
.ifNotExists()
|
|
70
79
|
.on('ai_tool_call')
|
|
71
|
-
.column('message_id')
|
|
72
|
-
.execute();
|
|
80
|
+
.column('message_id'));
|
|
73
81
|
await this.db.schema
|
|
74
82
|
.createTable('ai_working_memory')
|
|
75
83
|
.ifNotExists()
|
|
76
|
-
.addColumn('id', '
|
|
77
|
-
.addColumn('scope', '
|
|
84
|
+
.addColumn('id', 'varchar(255)', (col) => col.notNull())
|
|
85
|
+
.addColumn('scope', 'varchar(50)', (col) => col.notNull())
|
|
78
86
|
.addColumn('data', 'text', (col) => col.notNull())
|
|
79
87
|
.addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
80
88
|
.addPrimaryKeyConstraint('ai_working_memory_pk', ['id', 'scope'])
|
|
@@ -82,25 +90,24 @@ export class KyselyAIStorageService {
|
|
|
82
90
|
await this.db.schema
|
|
83
91
|
.createTable('ai_run')
|
|
84
92
|
.ifNotExists()
|
|
85
|
-
.addColumn('run_id', '
|
|
86
|
-
.addColumn('agent_name', '
|
|
87
|
-
.addColumn('thread_id', '
|
|
88
|
-
.addColumn('resource_id', '
|
|
89
|
-
.addColumn('status', '
|
|
93
|
+
.addColumn('run_id', 'varchar(36)', (col) => col.primaryKey())
|
|
94
|
+
.addColumn('agent_name', 'varchar(255)', (col) => col.notNull())
|
|
95
|
+
.addColumn('thread_id', 'varchar(36)', (col) => col.notNull().references('ai_threads.id').onDelete('cascade'))
|
|
96
|
+
.addColumn('resource_id', 'varchar(255)', (col) => col.notNull())
|
|
97
|
+
.addColumn('status', 'varchar(50)', (col) => col.notNull().defaultTo('running'))
|
|
98
|
+
.addColumn('error_message', 'text')
|
|
90
99
|
.addColumn('suspend_reason', 'text')
|
|
91
100
|
.addColumn('missing_rpcs', 'text')
|
|
92
101
|
.addColumn('usage_input_tokens', 'integer', (col) => col.notNull().defaultTo(0))
|
|
93
102
|
.addColumn('usage_output_tokens', 'integer', (col) => col.notNull().defaultTo(0))
|
|
94
|
-
.addColumn('usage_model', '
|
|
103
|
+
.addColumn('usage_model', 'varchar(255)', (col) => col.notNull().defaultTo(''))
|
|
95
104
|
.addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
96
105
|
.addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
97
106
|
.execute();
|
|
98
|
-
await this.db.schema
|
|
107
|
+
await this.createIndexSafe(this.db.schema
|
|
99
108
|
.createIndex('idx_ai_run_thread')
|
|
100
|
-
.ifNotExists()
|
|
101
109
|
.on('ai_run')
|
|
102
|
-
.columns(['thread_id', 'created_at'])
|
|
103
|
-
.execute();
|
|
110
|
+
.columns(['thread_id', 'created_at']));
|
|
104
111
|
this.initialized = true;
|
|
105
112
|
}
|
|
106
113
|
async createThread(resourceId, options) {
|
|
@@ -219,10 +226,26 @@ export class KyselyAIStorageService {
|
|
|
219
226
|
}
|
|
220
227
|
const messages = [];
|
|
221
228
|
for (const row of msgResult) {
|
|
229
|
+
const rawContent = row.content;
|
|
230
|
+
let parsedContent = rawContent ?? undefined;
|
|
231
|
+
if (rawContent) {
|
|
232
|
+
try {
|
|
233
|
+
const parsed = JSON.parse(rawContent);
|
|
234
|
+
if (Array.isArray(parsed)) {
|
|
235
|
+
parsedContent = parsed;
|
|
236
|
+
}
|
|
237
|
+
else if (typeof parsed === 'string') {
|
|
238
|
+
parsedContent = parsed;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Not JSON, use raw string
|
|
243
|
+
}
|
|
244
|
+
}
|
|
222
245
|
const msg = {
|
|
223
246
|
id: row.id,
|
|
224
247
|
role: row.role,
|
|
225
|
-
content:
|
|
248
|
+
content: parsedContent,
|
|
226
249
|
createdAt: new Date(row.created_at),
|
|
227
250
|
};
|
|
228
251
|
const tcs = tcByMessage.get(msg.id);
|
|
@@ -264,7 +287,7 @@ export class KyselyAIStorageService {
|
|
|
264
287
|
id: msg.id,
|
|
265
288
|
thread_id: threadId,
|
|
266
289
|
role: msg.role,
|
|
267
|
-
content: msg.content
|
|
290
|
+
content: msg.content != null ? JSON.stringify(msg.content) : null,
|
|
268
291
|
created_at: msg.createdAt ?? new Date(),
|
|
269
292
|
})))
|
|
270
293
|
.execute();
|
|
@@ -335,6 +358,7 @@ export class KyselyAIStorageService {
|
|
|
335
358
|
thread_id: run.threadId,
|
|
336
359
|
resource_id: run.resourceId,
|
|
337
360
|
status: run.status,
|
|
361
|
+
error_message: run.errorMessage ?? null,
|
|
338
362
|
suspend_reason: run.suspendReason ?? null,
|
|
339
363
|
missing_rpcs: run.missingRpcs ? JSON.stringify(run.missingRpcs) : null,
|
|
340
364
|
usage_input_tokens: run.usage.inputTokens,
|
|
@@ -354,6 +378,9 @@ export class KyselyAIStorageService {
|
|
|
354
378
|
if (updates.status !== undefined) {
|
|
355
379
|
setValues.status = updates.status;
|
|
356
380
|
}
|
|
381
|
+
if (updates.errorMessage !== undefined) {
|
|
382
|
+
setValues.error_message = updates.errorMessage;
|
|
383
|
+
}
|
|
357
384
|
if (updates.suspendReason !== undefined) {
|
|
358
385
|
setValues.suspend_reason = updates.suspendReason;
|
|
359
386
|
}
|
|
@@ -398,6 +425,7 @@ export class KyselyAIStorageService {
|
|
|
398
425
|
'thread_id',
|
|
399
426
|
'resource_id',
|
|
400
427
|
'status',
|
|
428
|
+
'error_message',
|
|
401
429
|
'suspend_reason',
|
|
402
430
|
'missing_rpcs',
|
|
403
431
|
'usage_input_tokens',
|
|
@@ -435,6 +463,7 @@ export class KyselyAIStorageService {
|
|
|
435
463
|
'thread_id',
|
|
436
464
|
'resource_id',
|
|
437
465
|
'status',
|
|
466
|
+
'error_message',
|
|
438
467
|
'suspend_reason',
|
|
439
468
|
'missing_rpcs',
|
|
440
469
|
'usage_input_tokens',
|
|
@@ -522,6 +551,7 @@ export class KyselyAIStorageService {
|
|
|
522
551
|
threadId: row.thread_id,
|
|
523
552
|
resourceId: row.resource_id,
|
|
524
553
|
status: row.status,
|
|
554
|
+
errorMessage: row.error_message ?? undefined,
|
|
525
555
|
suspendReason: row.suspend_reason,
|
|
526
556
|
missingRpcs: parseJson(row.missing_rpcs),
|
|
527
557
|
pendingApprovals,
|
|
@@ -2,7 +2,7 @@ import type { DeploymentService, DeploymentServiceConfig, DeploymentConfig, Depl
|
|
|
2
2
|
import type { Kysely } from 'kysely';
|
|
3
3
|
import type { KyselyPikkuDB } from './kysely-tables.js';
|
|
4
4
|
export declare class KyselyDeploymentService implements DeploymentService {
|
|
5
|
-
|
|
5
|
+
protected db: Kysely<KyselyPikkuDB>;
|
|
6
6
|
private initialized;
|
|
7
7
|
private heartbeatTimer?;
|
|
8
8
|
private deploymentConfig?;
|
|
@@ -13,5 +13,6 @@ export declare class KyselyDeploymentService implements DeploymentService {
|
|
|
13
13
|
start(config: DeploymentConfig): Promise<void>;
|
|
14
14
|
stop(): Promise<void>;
|
|
15
15
|
findFunction(name: string): Promise<DeploymentInfo[]>;
|
|
16
|
+
private createIndexSafe;
|
|
16
17
|
private sendHeartbeat;
|
|
17
18
|
}
|
|
@@ -37,18 +37,16 @@ export class KyselyDeploymentService {
|
|
|
37
37
|
'function_name',
|
|
38
38
|
])
|
|
39
39
|
.execute();
|
|
40
|
-
await this.db.schema
|
|
40
|
+
await this.createIndexSafe(this.db.schema
|
|
41
41
|
.createIndex('idx_pikku_deployments_heartbeat')
|
|
42
42
|
.ifNotExists()
|
|
43
43
|
.on('pikku_deployments')
|
|
44
|
-
.column('last_heartbeat')
|
|
45
|
-
|
|
46
|
-
await this.db.schema
|
|
44
|
+
.column('last_heartbeat'));
|
|
45
|
+
await this.createIndexSafe(this.db.schema
|
|
47
46
|
.createIndex('idx_pikku_deployment_functions_name')
|
|
48
47
|
.ifNotExists()
|
|
49
48
|
.on('pikku_deployment_functions')
|
|
50
|
-
.column('function_name')
|
|
51
|
-
.execute();
|
|
49
|
+
.column('function_name'));
|
|
52
50
|
this.initialized = true;
|
|
53
51
|
}
|
|
54
52
|
async start(config) {
|
|
@@ -111,6 +109,20 @@ export class KyselyDeploymentService {
|
|
|
111
109
|
endpoint: row.endpoint,
|
|
112
110
|
}));
|
|
113
111
|
}
|
|
112
|
+
async createIndexSafe(builder) {
|
|
113
|
+
try {
|
|
114
|
+
await builder.execute();
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
if (e?.code === 'ER_DUP_KEYNAME' || e?.errno === 1061)
|
|
118
|
+
return;
|
|
119
|
+
if (e?.code === '42P07')
|
|
120
|
+
return;
|
|
121
|
+
if (e?.message?.includes('already exists'))
|
|
122
|
+
return;
|
|
123
|
+
throw e;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
114
126
|
async sendHeartbeat() {
|
|
115
127
|
if (!this.deploymentConfig)
|
|
116
128
|
return;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SecretService } from '@pikku/core/services';
|
|
2
|
+
import type { Kysely } from 'kysely';
|
|
3
|
+
import type { KyselyPikkuDB } from './kysely-tables.js';
|
|
4
|
+
export interface KyselySecretServiceConfig {
|
|
5
|
+
key: string;
|
|
6
|
+
keyVersion?: number;
|
|
7
|
+
previousKey?: string;
|
|
8
|
+
audit?: boolean;
|
|
9
|
+
auditReads?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare class KyselySecretService implements SecretService {
|
|
12
|
+
private db;
|
|
13
|
+
private initialized;
|
|
14
|
+
private key;
|
|
15
|
+
private keyVersion;
|
|
16
|
+
private previousKey?;
|
|
17
|
+
private audit;
|
|
18
|
+
private auditReads;
|
|
19
|
+
constructor(db: Kysely<KyselyPikkuDB>, config: KyselySecretServiceConfig);
|
|
20
|
+
init(): Promise<void>;
|
|
21
|
+
private logAudit;
|
|
22
|
+
private getKEK;
|
|
23
|
+
getSecret(key: string): Promise<string>;
|
|
24
|
+
getSecretJSON<R = {}>(key: string): Promise<R>;
|
|
25
|
+
hasSecret(key: string): Promise<boolean>;
|
|
26
|
+
setSecretJSON(key: string, value: unknown): Promise<void>;
|
|
27
|
+
deleteSecret(key: string): Promise<void>;
|
|
28
|
+
rotateKEK(): Promise<number>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { sql } from 'kysely';
|
|
2
|
+
import { envelopeEncrypt, envelopeDecrypt, envelopeRewrap, } from '@pikku/core/crypto-utils';
|
|
3
|
+
export class KyselySecretService {
|
|
4
|
+
db;
|
|
5
|
+
initialized = false;
|
|
6
|
+
key;
|
|
7
|
+
keyVersion;
|
|
8
|
+
previousKey;
|
|
9
|
+
audit;
|
|
10
|
+
auditReads;
|
|
11
|
+
constructor(db, config) {
|
|
12
|
+
this.db = db;
|
|
13
|
+
this.key = config.key;
|
|
14
|
+
this.keyVersion = config.keyVersion ?? 1;
|
|
15
|
+
this.previousKey = config.previousKey;
|
|
16
|
+
this.audit = config.audit ?? false;
|
|
17
|
+
this.auditReads = config.auditReads ?? false;
|
|
18
|
+
}
|
|
19
|
+
async init() {
|
|
20
|
+
if (this.initialized)
|
|
21
|
+
return;
|
|
22
|
+
await this.db.schema
|
|
23
|
+
.createTable('secrets')
|
|
24
|
+
.ifNotExists()
|
|
25
|
+
.addColumn('key', 'varchar(255)', (col) => col.primaryKey())
|
|
26
|
+
.addColumn('ciphertext', 'text', (col) => col.notNull())
|
|
27
|
+
.addColumn('wrapped_dek', 'text', (col) => col.notNull())
|
|
28
|
+
.addColumn('key_version', 'integer', (col) => col.notNull())
|
|
29
|
+
.addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
30
|
+
.addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
31
|
+
.execute();
|
|
32
|
+
if (this.audit) {
|
|
33
|
+
await this.db.schema
|
|
34
|
+
.createTable('secrets_audit')
|
|
35
|
+
.ifNotExists()
|
|
36
|
+
.addColumn('id', 'varchar(36)', (col) => col.primaryKey())
|
|
37
|
+
.addColumn('secret_key', 'varchar(255)', (col) => col.notNull())
|
|
38
|
+
.addColumn('action', 'varchar(20)', (col) => col.notNull())
|
|
39
|
+
.addColumn('performed_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
|
|
40
|
+
.execute();
|
|
41
|
+
}
|
|
42
|
+
this.initialized = true;
|
|
43
|
+
}
|
|
44
|
+
async logAudit(secretKey, action) {
|
|
45
|
+
if (!this.audit)
|
|
46
|
+
return;
|
|
47
|
+
if (action === 'read' && !this.auditReads)
|
|
48
|
+
return;
|
|
49
|
+
await this.db
|
|
50
|
+
.insertInto('secrets_audit')
|
|
51
|
+
.values({
|
|
52
|
+
id: crypto.randomUUID(),
|
|
53
|
+
secret_key: secretKey,
|
|
54
|
+
action,
|
|
55
|
+
performed_at: new Date().toISOString(),
|
|
56
|
+
})
|
|
57
|
+
.execute();
|
|
58
|
+
}
|
|
59
|
+
getKEK(version) {
|
|
60
|
+
if (version === this.keyVersion)
|
|
61
|
+
return this.key;
|
|
62
|
+
if (this.previousKey)
|
|
63
|
+
return this.previousKey;
|
|
64
|
+
throw new Error(`No KEK available for key_version ${version}`);
|
|
65
|
+
}
|
|
66
|
+
async getSecret(key) {
|
|
67
|
+
const row = await this.db
|
|
68
|
+
.selectFrom('secrets')
|
|
69
|
+
.select(['ciphertext', 'wrapped_dek', 'key_version'])
|
|
70
|
+
.where('key', '=', key)
|
|
71
|
+
.executeTakeFirst();
|
|
72
|
+
if (!row)
|
|
73
|
+
throw new Error(`Secret not found: ${key}`);
|
|
74
|
+
const kek = this.getKEK(row.key_version);
|
|
75
|
+
const result = await envelopeDecrypt(kek, row.ciphertext, row.wrapped_dek);
|
|
76
|
+
await this.logAudit(key, 'read');
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
async getSecretJSON(key) {
|
|
80
|
+
const raw = await this.getSecret(key);
|
|
81
|
+
return JSON.parse(raw);
|
|
82
|
+
}
|
|
83
|
+
async hasSecret(key) {
|
|
84
|
+
const row = await this.db
|
|
85
|
+
.selectFrom('secrets')
|
|
86
|
+
.select('key')
|
|
87
|
+
.where('key', '=', key)
|
|
88
|
+
.executeTakeFirst();
|
|
89
|
+
return !!row;
|
|
90
|
+
}
|
|
91
|
+
async setSecretJSON(key, value) {
|
|
92
|
+
const plaintext = JSON.stringify(value);
|
|
93
|
+
const { ciphertext, wrappedDEK } = await envelopeEncrypt(this.key, plaintext);
|
|
94
|
+
const now = new Date().toISOString();
|
|
95
|
+
await this.db
|
|
96
|
+
.insertInto('secrets')
|
|
97
|
+
.values({
|
|
98
|
+
key,
|
|
99
|
+
ciphertext,
|
|
100
|
+
wrapped_dek: wrappedDEK,
|
|
101
|
+
key_version: this.keyVersion,
|
|
102
|
+
created_at: now,
|
|
103
|
+
updated_at: now,
|
|
104
|
+
})
|
|
105
|
+
.onConflict((oc) => oc.column('key').doUpdateSet({
|
|
106
|
+
ciphertext,
|
|
107
|
+
wrapped_dek: wrappedDEK,
|
|
108
|
+
key_version: this.keyVersion,
|
|
109
|
+
updated_at: now,
|
|
110
|
+
}))
|
|
111
|
+
.execute();
|
|
112
|
+
await this.logAudit(key, 'write');
|
|
113
|
+
}
|
|
114
|
+
async deleteSecret(key) {
|
|
115
|
+
await this.db.deleteFrom('secrets').where('key', '=', key).execute();
|
|
116
|
+
await this.logAudit(key, 'delete');
|
|
117
|
+
}
|
|
118
|
+
async rotateKEK() {
|
|
119
|
+
if (!this.previousKey) {
|
|
120
|
+
throw new Error('No previousKey configured — nothing to rotate from');
|
|
121
|
+
}
|
|
122
|
+
const rows = await this.db
|
|
123
|
+
.selectFrom('secrets')
|
|
124
|
+
.select(['key', 'wrapped_dek'])
|
|
125
|
+
.where('key_version', '<', this.keyVersion)
|
|
126
|
+
.execute();
|
|
127
|
+
for (const row of rows) {
|
|
128
|
+
const newWrappedDEK = await envelopeRewrap(this.previousKey, this.key, row.wrapped_dek);
|
|
129
|
+
await this.db
|
|
130
|
+
.updateTable('secrets')
|
|
131
|
+
.set({
|
|
132
|
+
wrapped_dek: newWrappedDEK,
|
|
133
|
+
key_version: this.keyVersion,
|
|
134
|
+
updated_at: new Date().toISOString(),
|
|
135
|
+
})
|
|
136
|
+
.where('key', '=', row.key)
|
|
137
|
+
.execute();
|
|
138
|
+
await this.logAudit(row.key, 'rotate');
|
|
139
|
+
}
|
|
140
|
+
return rows.length;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Generated } from 'kysely';
|
|
2
|
+
import type { WorkflowStatus, StepStatus, WorkflowVersionStatus } from '@pikku/core/workflow';
|
|
2
3
|
export interface ChannelsTable {
|
|
3
4
|
channel_id: string;
|
|
4
5
|
channel_name: string;
|
|
@@ -14,7 +15,7 @@ export interface ChannelSubscriptionsTable {
|
|
|
14
15
|
export interface WorkflowRunsTable {
|
|
15
16
|
workflow_run_id: Generated<string>;
|
|
16
17
|
workflow: string;
|
|
17
|
-
status:
|
|
18
|
+
status: WorkflowStatus;
|
|
18
19
|
input: string;
|
|
19
20
|
output: string | null;
|
|
20
21
|
error: string | null;
|
|
@@ -31,7 +32,7 @@ export interface WorkflowStepTable {
|
|
|
31
32
|
step_name: string;
|
|
32
33
|
rpc_name: string | null;
|
|
33
34
|
data: string | null;
|
|
34
|
-
status: Generated<
|
|
35
|
+
status: Generated<StepStatus>;
|
|
35
36
|
result: string | null;
|
|
36
37
|
error: string | null;
|
|
37
38
|
branch_taken: string | null;
|
|
@@ -43,7 +44,7 @@ export interface WorkflowStepTable {
|
|
|
43
44
|
export interface WorkflowStepHistoryTable {
|
|
44
45
|
history_id: Generated<string>;
|
|
45
46
|
workflow_step_id: string;
|
|
46
|
-
status:
|
|
47
|
+
status: StepStatus;
|
|
47
48
|
result: string | null;
|
|
48
49
|
error: string | null;
|
|
49
50
|
created_at: Generated<Date>;
|
|
@@ -57,6 +58,7 @@ export interface WorkflowVersionsTable {
|
|
|
57
58
|
graph_hash: string;
|
|
58
59
|
graph: string;
|
|
59
60
|
source: string;
|
|
61
|
+
status: Generated<WorkflowVersionStatus>;
|
|
60
62
|
created_at: Generated<Date>;
|
|
61
63
|
}
|
|
62
64
|
export interface AIThreadsTable {
|
|
@@ -70,7 +72,7 @@ export interface AIThreadsTable {
|
|
|
70
72
|
export interface AIMessageTable {
|
|
71
73
|
id: string;
|
|
72
74
|
thread_id: string;
|
|
73
|
-
role:
|
|
75
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
74
76
|
content: string | null;
|
|
75
77
|
created_at: Generated<Date>;
|
|
76
78
|
}
|
|
@@ -82,8 +84,8 @@ export interface AIToolCallTable {
|
|
|
82
84
|
tool_name: string;
|
|
83
85
|
args: string;
|
|
84
86
|
result: string | null;
|
|
85
|
-
approval_status:
|
|
86
|
-
approval_type:
|
|
87
|
+
approval_status: 'approved' | 'denied' | 'pending' | null;
|
|
88
|
+
approval_type: 'agent-call' | 'tool-call' | null;
|
|
87
89
|
agent_run_id: string | null;
|
|
88
90
|
display_tool_name: string | null;
|
|
89
91
|
display_args: string | null;
|
|
@@ -100,8 +102,9 @@ export interface AIRunTable {
|
|
|
100
102
|
agent_name: string;
|
|
101
103
|
thread_id: string;
|
|
102
104
|
resource_id: string;
|
|
103
|
-
status: Generated<
|
|
104
|
-
|
|
105
|
+
status: Generated<'running' | 'suspended' | 'completed' | 'failed'>;
|
|
106
|
+
error_message: string | null;
|
|
107
|
+
suspend_reason: 'approval' | 'rpc-missing' | null;
|
|
105
108
|
missing_rpcs: string | null;
|
|
106
109
|
usage_input_tokens: Generated<number>;
|
|
107
110
|
usage_output_tokens: Generated<number>;
|
|
@@ -119,6 +122,20 @@ export interface PikkuDeploymentFunctionsTable {
|
|
|
119
122
|
deployment_id: string;
|
|
120
123
|
function_name: string;
|
|
121
124
|
}
|
|
125
|
+
export interface SecretsTable {
|
|
126
|
+
key: string;
|
|
127
|
+
ciphertext: string;
|
|
128
|
+
wrapped_dek: string;
|
|
129
|
+
key_version: number;
|
|
130
|
+
created_at: Generated<Date>;
|
|
131
|
+
updated_at: Generated<Date>;
|
|
132
|
+
}
|
|
133
|
+
export interface SecretsAuditTable {
|
|
134
|
+
id: string;
|
|
135
|
+
secret_key: string;
|
|
136
|
+
action: string;
|
|
137
|
+
performed_at: Generated<Date>;
|
|
138
|
+
}
|
|
122
139
|
export interface KyselyPikkuDB {
|
|
123
140
|
channels: ChannelsTable;
|
|
124
141
|
channel_subscriptions: ChannelSubscriptionsTable;
|
|
@@ -133,4 +150,6 @@ export interface KyselyPikkuDB {
|
|
|
133
150
|
ai_run: AIRunTable;
|
|
134
151
|
pikku_deployments: PikkuDeploymentsTable;
|
|
135
152
|
pikku_deployment_functions: PikkuDeploymentFunctionsTable;
|
|
153
|
+
secrets: SecretsTable;
|
|
154
|
+
secrets_audit: SecretsAuditTable;
|
|
136
155
|
}
|
|
@@ -24,6 +24,11 @@ export declare class KyselyWorkflowRunService implements WorkflowRunService {
|
|
|
24
24
|
graph: any;
|
|
25
25
|
source: string;
|
|
26
26
|
} | null>;
|
|
27
|
+
getAIGeneratedWorkflows(agentName?: string): Promise<Array<{
|
|
28
|
+
workflowName: string;
|
|
29
|
+
graphHash: string;
|
|
30
|
+
graph: any;
|
|
31
|
+
}>>;
|
|
27
32
|
deleteRun(id: string): Promise<boolean>;
|
|
28
33
|
private mapRunRow;
|
|
29
34
|
}
|