@pikku/core 0.12.80 → 0.12.83
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 +345 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/function/function-runner.js +2 -5
- package/dist/function/index.d.ts +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/index.js +3 -3
- package/dist/pikku-state.js +4 -0
- package/dist/services/ai-agent-runner-service.d.ts +7 -0
- package/dist/services/ai-run-state-service.d.ts +10 -0
- package/dist/services/in-memory-ai-run-state-service.d.ts +5 -1
- package/dist/services/in-memory-ai-run-state-service.js +9 -0
- package/dist/services/index.d.ts +15 -16
- package/dist/services/index.js +5 -5
- package/dist/services/meta-service.d.ts +2 -1
- package/dist/services/scoped-credential-service.d.ts +21 -0
- package/dist/services/scoped-credential-service.js +53 -0
- package/dist/testing/service-tests/ai-storage-service-tests.js +76 -0
- package/dist/types/core.types.d.ts +2 -3
- package/dist/types/state.types.d.ts +19 -1
- package/dist/wirings/actor-flow/index.d.ts +1 -1
- package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +58 -0
- package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
- package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
- package/dist/wirings/ai-agent/ai-agent-memory.js +34 -17
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
- package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -0
- package/dist/wirings/ai-agent/ai-agent-runner.js +61 -40
- package/dist/wirings/ai-agent/ai-agent-stream.js +89 -36
- package/dist/wirings/ai-agent/ai-agent-turn.d.ts +1 -0
- package/dist/wirings/ai-agent/ai-agent-turn.js +1 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +46 -1
- package/dist/wirings/ai-agent/index.d.ts +8 -7
- package/dist/wirings/ai-agent/index.js +5 -4
- package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
- package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
- package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
- package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
- package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
- package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
- package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
- package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
- package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
- package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
- package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
- package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
- package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
- package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
- package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
- package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
- package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
- package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
- package/dist/wirings/ai-scorer/index.d.ts +6 -0
- package/dist/wirings/ai-scorer/index.js +5 -0
- package/dist/wirings/channel/index.d.ts +5 -6
- package/dist/wirings/channel/index.js +3 -4
- package/dist/wirings/channel/local/local-channel-runner.js +8 -1
- package/dist/wirings/cli/channel/cli-raw-channel-runner.js +9 -1
- package/dist/wirings/cli/channel/index.d.ts +1 -2
- package/dist/wirings/cli/channel/index.js +0 -1
- package/dist/wirings/cli/cli-runner.js +13 -1
- package/dist/wirings/credential/index.d.ts +1 -1
- package/dist/wirings/gateway/index.d.ts +1 -1
- package/dist/wirings/http/http-runner.js +8 -2
- package/dist/wirings/http/index.d.ts +1 -2
- package/dist/wirings/mcp/index.d.ts +1 -1
- package/dist/wirings/mcp/mcp-runner.d.ts +15 -0
- package/dist/wirings/mcp/mcp-runner.js +18 -5
- package/dist/wirings/persona/index.d.ts +3 -4
- package/dist/wirings/persona/index.js +2 -3
- package/dist/wirings/queue/index.d.ts +1 -3
- package/dist/wirings/queue/index.js +1 -3
- package/dist/wirings/rpc/addon-runner.d.ts +8 -0
- package/dist/wirings/rpc/addon-runner.js +31 -3
- package/dist/wirings/rpc/rpc-runner.js +4 -0
- package/dist/wirings/rpc/rpc-types.d.ts +8 -0
- package/dist/wirings/rpc/wire-addon.d.ts +25 -0
- package/dist/wirings/rpc/wire-addon.js +8 -0
- package/dist/wirings/scheduler/index.d.ts +1 -1
- package/dist/wirings/trigger/index.d.ts +1 -1
- package/dist/wirings/virtual-user/index.d.ts +5 -6
- package/dist/wirings/virtual-user/index.js +2 -4
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +85 -15
- package/dist/wirings/workflow/feature.d.ts +2 -1
- package/dist/wirings/workflow/index.d.ts +5 -16
- package/dist/wirings/workflow/index.js +1 -9
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -7
- package/dist/wirings/workflow/pikku-scenario-service.js +48 -13
- package/dist/wirings/workflow/pikku-workflow-service.js +17 -3
- package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
- package/dist/wirings/workflow/scenario.types.d.ts +37 -0
- package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
- package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
- package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
- package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
- package/dist/wirings/workflow/workflow-approval.d.ts +29 -1
- package/dist/wirings/workflow/workflow-approval.js +65 -2
- package/dist/wirings/workflow/workflow-run-ownership.d.ts +2 -1
- package/dist/wirings/workflow/workflow-run-ownership.js +2 -1
- package/dist/wirings/workflow/workflow.types.d.ts +2 -37
- package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -0
- package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -0
- package/knowledge/decisions/internals/index.md +30 -3
- package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +115 -0
- package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +37 -0
- package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +30 -14
- package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -0
- package/knowledge/decisions/security/index.md +3 -1
- package/knowledge/questions/index.md +1 -1
- package/package.json +3 -2
- package/scripts/generate-api-report.mts +143 -18
- package/src/api-report.test.ts +2 -2
- package/src/errors/index.ts +1 -1
- package/src/function/function-runner.test.ts +52 -0
- package/src/function/function-runner.ts +5 -9
- package/src/function/index.ts +0 -2
- package/src/index.ts +0 -35
- package/src/pikku-state.ts +5 -0
- package/src/public-surface.json +81 -118
- package/src/services/ai-agent-runner-service.ts +12 -1
- package/src/services/ai-run-state-service.ts +11 -0
- package/src/services/in-memory-ai-run-state-service.ts +13 -0
- package/src/services/index.ts +7 -58
- package/src/services/meta-service.ts +2 -4
- package/src/services/scoped-credential-service.test.ts +86 -0
- package/src/services/scoped-credential-service.ts +63 -0
- package/src/testing/service-tests/ai-storage-service-tests.ts +93 -0
- package/src/types/core.types.ts +4 -7
- package/src/types/state.types.ts +21 -1
- package/src/wirings/actor-flow/index.ts +0 -3
- package/src/wirings/ai-agent/ai-agent-finalize.test.ts +186 -0
- package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
- package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +54 -38
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
- package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +71 -40
- package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +116 -54
- package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
- package/src/wirings/ai-agent/ai-agent-turn.ts +1 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +64 -4
- package/src/wirings/ai-agent/index.ts +2 -16
- package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
- package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
- package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
- package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
- package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
- package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
- package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
- package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
- package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
- package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
- package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
- package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
- package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
- package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
- package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
- package/src/wirings/ai-scorer/index.ts +24 -0
- package/src/wirings/channel/index.ts +1 -20
- package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
- package/src/wirings/channel/local/local-channel-runner.ts +8 -1
- package/src/wirings/cli/channel/cli-raw-channel-runner.test.ts +23 -0
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
- package/src/wirings/cli/channel/index.ts +0 -7
- package/src/wirings/cli/cli-runner.test.ts +68 -0
- package/src/wirings/cli/cli-runner.ts +18 -1
- package/src/wirings/credential/index.ts +0 -1
- package/src/wirings/gateway/index.ts +0 -3
- package/src/wirings/http/http-runner.test.ts +66 -0
- package/src/wirings/http/http-runner.ts +10 -2
- package/src/wirings/http/index.ts +1 -1
- package/src/wirings/mcp/index.ts +0 -1
- package/src/wirings/mcp/mcp-runner.test.ts +181 -0
- package/src/wirings/mcp/mcp-runner.ts +35 -5
- package/src/wirings/persona/index.ts +0 -8
- package/src/wirings/queue/index.ts +0 -14
- package/src/wirings/rpc/addon-runner.ts +62 -3
- package/src/wirings/rpc/addon-secrets.test.ts +391 -0
- package/src/wirings/rpc/rpc-runner.test.ts +2 -0
- package/src/wirings/rpc/rpc-runner.ts +4 -0
- package/src/wirings/rpc/rpc-types.ts +8 -0
- package/src/wirings/rpc/wire-addon.ts +33 -0
- package/src/wirings/scheduler/index.ts +0 -1
- package/src/wirings/trigger/index.ts +0 -1
- package/src/wirings/virtual-user/index.ts +0 -16
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +96 -16
- package/src/wirings/workflow/feature.ts +2 -5
- package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
- package/src/wirings/workflow/index.ts +2 -68
- package/src/wirings/workflow/pikku-scenario-service.ts +81 -16
- package/src/wirings/workflow/pikku-workflow-service.test.ts +13 -12
- package/src/wirings/workflow/pikku-workflow-service.ts +28 -4
- package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
- package/src/wirings/workflow/scenario-hooks.test.ts +3 -2
- package/src/wirings/workflow/scenario-step.types.ts +8 -0
- package/src/wirings/workflow/scenario.types.ts +63 -0
- package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
- package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
- package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
- package/src/wirings/workflow/workflow-approval.ts +113 -9
- package/src/wirings/workflow/workflow-run-authority.test.ts +12 -15
- package/src/wirings/workflow/workflow-run-ownership.ts +2 -1
- package/src/wirings/workflow/workflow.types.ts +1 -63
- package/src/wirings-stay-decoupled.test.ts +6 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/internal.d.ts +0 -3
- package/dist/internal.js +0 -2
- package/dist/middleware/timeout.d.ts +0 -9
- package/dist/middleware/timeout.js +0 -15
- package/dist/pikku-response.d.ts +0 -6
- package/dist/pikku-response.js +0 -6
- package/dist/services/gopass-secrets.d.ts +0 -15
- package/dist/services/gopass-secrets.js +0 -76
- package/dist/services/http-scenario-actors.d.ts +0 -75
- package/dist/services/http-scenario-actors.js +0 -195
- package/dist/services/http-user-flow-actors.d.ts +0 -67
- package/dist/services/http-user-flow-actors.js +0 -193
- package/dist/services/scenario-actors-service.d.ts +0 -127
- package/dist/services/scenario-actors-service.js +0 -40
- package/dist/services/user-flow-actors-service.d.ts +0 -39
- package/dist/wirings/credential/wire-credential.d.ts +0 -48
- package/dist/wirings/credential/wire-credential.js +0 -47
- package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
- package/dist/wirings/oauth2/oauth2-client.js +0 -263
- package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
- package/dist/wirings/oauth2/oauth2-routes.js +0 -146
- package/dist/wirings/scope/wire-scope.d.ts +0 -33
- package/dist/wirings/scope/wire-scope.js +0 -32
- package/dist/wirings/workflow/dsl/index.d.ts +0 -5
- package/dist/wirings/workflow/dsl/index.js +0 -4
- package/dist/wirings/workflow/graph/index.d.ts +0 -5
- package/dist/wirings/workflow/graph/index.js +0 -4
- /package/dist/{services/user-flow-actors-service.js → wirings/workflow/scenario.types.js} +0 -0
|
@@ -396,6 +396,74 @@ describe('CLI Runner', () => {
|
|
|
396
396
|
const programs = pikkuState(null, 'cli', 'programs')
|
|
397
397
|
assert.strictEqual(programs['nonexistent'], undefined)
|
|
398
398
|
})
|
|
399
|
+
|
|
400
|
+
test('enforces command-level permissions declared on a CLI command', async () => {
|
|
401
|
+
let functionRan = false
|
|
402
|
+
|
|
403
|
+
pikkuState(null, 'cli', 'meta', {
|
|
404
|
+
programs: {
|
|
405
|
+
'sec-cli': {
|
|
406
|
+
program: 'sec-cli',
|
|
407
|
+
commands: {
|
|
408
|
+
secret: {
|
|
409
|
+
command: 'secret',
|
|
410
|
+
pikkuFuncId: 'secretFunc',
|
|
411
|
+
positionals: [],
|
|
412
|
+
options: {},
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
options: {},
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
renderers: {},
|
|
419
|
+
})
|
|
420
|
+
pikkuState(null, 'function', 'meta', {
|
|
421
|
+
secretFunc: {
|
|
422
|
+
pikkuFuncId: 'secretFunc',
|
|
423
|
+
inputSchemaName: null,
|
|
424
|
+
outputSchemaName: null,
|
|
425
|
+
sessionless: true,
|
|
426
|
+
},
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
wireCLI({
|
|
430
|
+
program: 'sec-cli',
|
|
431
|
+
commands: {
|
|
432
|
+
// command.func is a function-config object, so unwrapFunc reads the
|
|
433
|
+
// inner func's auth/permissions and previously dropped the
|
|
434
|
+
// command-level ones — the branch this fix restores.
|
|
435
|
+
secret: {
|
|
436
|
+
func: {
|
|
437
|
+
func: async () => {
|
|
438
|
+
functionRan = true
|
|
439
|
+
return {}
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
// A permission that always denies.
|
|
443
|
+
permissions: { deny: async () => false },
|
|
444
|
+
} as any,
|
|
445
|
+
},
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
const result = await runCLICommand({
|
|
449
|
+
program: 'sec-cli',
|
|
450
|
+
commandPath: ['secret'],
|
|
451
|
+
data: {},
|
|
452
|
+
singletonServices,
|
|
453
|
+
createWireServices,
|
|
454
|
+
}).catch((error) => ({ success: false, error }))
|
|
455
|
+
|
|
456
|
+
assert.equal(
|
|
457
|
+
functionRan,
|
|
458
|
+
false,
|
|
459
|
+
'the function must not run when a declared permission denies'
|
|
460
|
+
)
|
|
461
|
+
assert.notEqual(
|
|
462
|
+
(result as any).success,
|
|
463
|
+
true,
|
|
464
|
+
'the command must not report success when permission is denied'
|
|
465
|
+
)
|
|
466
|
+
})
|
|
399
467
|
})
|
|
400
468
|
|
|
401
469
|
describe('pikkuCLIRender', () => {
|
|
@@ -169,7 +169,24 @@ function registerCLICommands(
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
// Merge the command-level auth/permissions into the registered config so
|
|
173
|
+
// they are actually enforced by the function runner. They were accepted by
|
|
174
|
+
// the types but dropped here, making a command's declared access control a
|
|
175
|
+
// silent no-op. Command-level wins over the handler's own, then falls back
|
|
176
|
+
// to it.
|
|
177
|
+
const unwrapped = unwrapFunc(command)
|
|
178
|
+
const commandAuth = typeof command === 'object' ? command.auth : undefined
|
|
179
|
+
const commandPermissions =
|
|
180
|
+
typeof command === 'object' ? command.permissions : undefined
|
|
181
|
+
addFunction(
|
|
182
|
+
funcName,
|
|
183
|
+
{
|
|
184
|
+
...unwrapped,
|
|
185
|
+
auth: commandAuth ?? unwrapped.auth,
|
|
186
|
+
permissions: commandPermissions ?? unwrapped.permissions,
|
|
187
|
+
},
|
|
188
|
+
currentMeta?.packageName
|
|
189
|
+
)
|
|
173
190
|
|
|
174
191
|
if (typeof command === 'object' && command.render) {
|
|
175
192
|
if (programs[program]) {
|
|
@@ -2,7 +2,6 @@ export { defineCredential } from './define-credential.js'
|
|
|
2
2
|
export { validateAndBuildCredentialDefinitionsMeta } from './validate-credential-definitions.js'
|
|
3
3
|
export type {
|
|
4
4
|
CoreCredential,
|
|
5
|
-
CredentialDefinitionMeta,
|
|
6
5
|
CredentialDefinitionsMeta,
|
|
7
6
|
CredentialDefinitions,
|
|
8
7
|
} from './credential.types.js'
|
|
@@ -6,13 +6,10 @@ export {
|
|
|
6
6
|
export type {
|
|
7
7
|
GatewayAdapter,
|
|
8
8
|
GatewayAdapterFactory,
|
|
9
|
-
GatewayAttachment,
|
|
10
9
|
GatewayInboundMessage,
|
|
11
10
|
GatewayOutboundMessage,
|
|
12
|
-
GatewayMeta,
|
|
13
11
|
GatewaysMeta,
|
|
14
12
|
GatewayTransportType,
|
|
15
13
|
CoreGateway,
|
|
16
|
-
PikkuGateway,
|
|
17
14
|
WebhookVerificationResult,
|
|
18
15
|
} from './gateway.types.js'
|
|
@@ -70,6 +70,11 @@ class TestResponse extends PikkuMockResponse {
|
|
|
70
70
|
return this
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
redirect(location: string, status: number = 302): this {
|
|
74
|
+
this.status(status)
|
|
75
|
+
return this.header('Location', location)
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
setMode(mode: 'stream') {
|
|
74
79
|
this.mode = mode
|
|
75
80
|
}
|
|
@@ -365,6 +370,67 @@ describe('http-runner helpers', () => {
|
|
|
365
370
|
assert.equal(response.statusCode, 204)
|
|
366
371
|
})
|
|
367
372
|
|
|
373
|
+
test('fetchData keeps a redirect status when the route returns undefined', async () => {
|
|
374
|
+
setRouteMeta('/redirect')
|
|
375
|
+
addFunction('pikku_func_name', {
|
|
376
|
+
func: async (_services: any, _data: any, { http }: any) => {
|
|
377
|
+
http?.response?.redirect('https://example.com/done', 302)
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
wireHTTP({
|
|
381
|
+
route: '/redirect',
|
|
382
|
+
method: 'get',
|
|
383
|
+
auth: false,
|
|
384
|
+
func: {
|
|
385
|
+
func: async (_services: any, _data: any, { http }: any) => {
|
|
386
|
+
http?.response?.redirect('https://example.com/done', 302)
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
})
|
|
390
|
+
httpRouter.initialize()
|
|
391
|
+
|
|
392
|
+
const request = new TestRequest('/redirect', 'get')
|
|
393
|
+
const response = new TestResponse()
|
|
394
|
+
|
|
395
|
+
await fetchData(request, response)
|
|
396
|
+
|
|
397
|
+
assert.equal(response.statusCode, 302)
|
|
398
|
+
assert.equal(
|
|
399
|
+
response.headersMap.get('Location'),
|
|
400
|
+
'https://example.com/done'
|
|
401
|
+
)
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
test('fetchData keeps a status the route set alongside a body', async () => {
|
|
405
|
+
setRouteMeta('/created')
|
|
406
|
+
addFunction('pikku_func_name', {
|
|
407
|
+
func: async (_services: any, _data: any, { http }: any) => {
|
|
408
|
+
http?.response?.status(201)
|
|
409
|
+
return { id: 'thing-1' }
|
|
410
|
+
},
|
|
411
|
+
})
|
|
412
|
+
wireHTTP({
|
|
413
|
+
route: '/created',
|
|
414
|
+
method: 'get',
|
|
415
|
+
auth: false,
|
|
416
|
+
func: {
|
|
417
|
+
func: async (_services: any, _data: any, { http }: any) => {
|
|
418
|
+
http?.response?.status(201)
|
|
419
|
+
return { id: 'thing-1' }
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
})
|
|
423
|
+
httpRouter.initialize()
|
|
424
|
+
|
|
425
|
+
const request = new TestRequest('/created', 'get')
|
|
426
|
+
const response = new TestResponse()
|
|
427
|
+
|
|
428
|
+
await fetchData(request, response)
|
|
429
|
+
|
|
430
|
+
assert.equal(response.statusCode, 201)
|
|
431
|
+
assert.deepEqual(response.jsonBody, { id: 'thing-1' })
|
|
432
|
+
})
|
|
433
|
+
|
|
368
434
|
test('fetchData writes binary responses when returnsJSON is false', async () => {
|
|
369
435
|
setRouteMeta('/binary')
|
|
370
436
|
wireHTTP({
|
|
@@ -283,6 +283,8 @@ const executeRoute = async (
|
|
|
283
283
|
hasSessionChanged: () => userSession.sessionChanged,
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
const statusBeforeRoute = http?.response?.statusCode
|
|
287
|
+
|
|
286
288
|
try {
|
|
287
289
|
result = await runPikkuFunc(
|
|
288
290
|
'http',
|
|
@@ -323,14 +325,20 @@ const executeRoute = async (
|
|
|
323
325
|
if (matchedRoute.route.sse) {
|
|
324
326
|
http?.response?.flushHeaders?.()
|
|
325
327
|
} else {
|
|
328
|
+
const statusSetByRoute = http?.response?.statusCode !== statusBeforeRoute
|
|
329
|
+
|
|
326
330
|
if (result instanceof Response) {
|
|
327
331
|
await applyWebResponse(http!.response!, result)
|
|
328
332
|
} else if (result === undefined || result === null) {
|
|
329
|
-
|
|
333
|
+
if (!statusSetByRoute) {
|
|
334
|
+
http?.response?.status(204)
|
|
335
|
+
}
|
|
330
336
|
} else if (route.returnsJSON === false) {
|
|
331
337
|
http?.response?.arrayBuffer(result)
|
|
332
338
|
} else {
|
|
333
|
-
|
|
339
|
+
if (!statusSetByRoute) {
|
|
340
|
+
http?.response?.status(200)
|
|
341
|
+
}
|
|
334
342
|
http?.response?.json(result)
|
|
335
343
|
}
|
|
336
344
|
}
|
|
@@ -2,7 +2,6 @@ export {
|
|
|
2
2
|
PikkuFetchHTTPRequest,
|
|
3
3
|
DEFAULT_MAX_BODY_SIZE,
|
|
4
4
|
} from './pikku-fetch-http-request.js'
|
|
5
|
-
export type { PikkuFetchHTTPRequestOptions } from './pikku-fetch-http-request.js'
|
|
6
5
|
export { PikkuFetchHTTPResponse } from './pikku-fetch-http-response.js'
|
|
7
6
|
export { logRoutes } from './log-http-routes.js'
|
|
8
7
|
|
|
@@ -19,6 +18,7 @@ export type {
|
|
|
19
18
|
HTTPRouteContract,
|
|
20
19
|
HTTPRouteMap,
|
|
21
20
|
HTTPWiringsMeta,
|
|
21
|
+
PikkuHTTP,
|
|
22
22
|
PikkuHTTPRequest,
|
|
23
23
|
PikkuHTTPResponse,
|
|
24
24
|
PikkuQuery,
|
package/src/wirings/mcp/index.ts
CHANGED
|
@@ -9,8 +9,11 @@ import {
|
|
|
9
9
|
wireMCPPrompt,
|
|
10
10
|
wireMCPResource,
|
|
11
11
|
} from './mcp-runner.js'
|
|
12
|
+
import { addGlobalMiddleware } from '../../middleware-runner.js'
|
|
12
13
|
import { addFunction } from '../../function/function-runner.js'
|
|
13
14
|
import { pikkuState, resetPikkuState } from '../../pikku-state.js'
|
|
15
|
+
import { pikkuMiddleware } from '../../types/core.types.js'
|
|
16
|
+
import { addTagMiddleware } from '../../middleware-runner.js'
|
|
14
17
|
|
|
15
18
|
const logger = {
|
|
16
19
|
debug: () => {},
|
|
@@ -166,6 +169,49 @@ describe('runMCPResource', () => {
|
|
|
166
169
|
},
|
|
167
170
|
})
|
|
168
171
|
})
|
|
172
|
+
|
|
173
|
+
test('runs a templated resource middleware when invoked by concrete uri', async () => {
|
|
174
|
+
let middlewareRan = false
|
|
175
|
+
// A tag-derived middleware group (this is where a tag-based auth gate lands).
|
|
176
|
+
addTagMiddleware('secure', [
|
|
177
|
+
pikkuMiddleware(async (_services, _wire, next) => {
|
|
178
|
+
middlewareRan = true
|
|
179
|
+
await next()
|
|
180
|
+
}),
|
|
181
|
+
])
|
|
182
|
+
pikkuState(null, 'mcp', 'resourcesMeta')['resource://secure/{id}'] = {
|
|
183
|
+
uri: 'resource://secure/{id}',
|
|
184
|
+
title: 'Secure',
|
|
185
|
+
description: 'Secure',
|
|
186
|
+
pikkuFuncId: 'secureResourceFunc',
|
|
187
|
+
inputSchema: null,
|
|
188
|
+
outputSchema: null,
|
|
189
|
+
// The resource's merged middleware metadata. It must resolve for a concrete
|
|
190
|
+
// request against the template, or the resource is reachable with its gate
|
|
191
|
+
// skipped.
|
|
192
|
+
middleware: [{ type: 'tag', tag: 'secure' }],
|
|
193
|
+
} as never
|
|
194
|
+
registerFunction('secureResourceFunc', async () => ({ ok: true }))
|
|
195
|
+
|
|
196
|
+
wireMCPResource({
|
|
197
|
+
uri: 'resource://secure/{id}',
|
|
198
|
+
title: 'Secure',
|
|
199
|
+
description: 'Secure',
|
|
200
|
+
func: { func: async () => ({ ok: true }) } as never,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
await runMCPResource(
|
|
204
|
+
{ jsonrpc: '2.0', id: 'req-2', params: {} },
|
|
205
|
+
{ mcp: mcpWire as never },
|
|
206
|
+
'resource://secure/99'
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
assert.equal(
|
|
210
|
+
middlewareRan,
|
|
211
|
+
true,
|
|
212
|
+
'the templated resource middleware must run for a concrete uri'
|
|
213
|
+
)
|
|
214
|
+
})
|
|
169
215
|
})
|
|
170
216
|
|
|
171
217
|
describe('runMCPTool', () => {
|
|
@@ -371,6 +417,141 @@ describe('runMCPTool', () => {
|
|
|
371
417
|
})
|
|
372
418
|
})
|
|
373
419
|
|
|
420
|
+
describe("MCP carries the caller's HTTP request", () => {
|
|
421
|
+
// Without this, every auth middleware bails on its first line — each one
|
|
422
|
+
// opens with `if (!http?.request) return`, so an MCP call arrived at the
|
|
423
|
+
// function with no session, and a tool fronting a session-requiring function
|
|
424
|
+
// answered 'Authentication required' to every caller. Nothing else was
|
|
425
|
+
// missing: global middleware already runs for MCP, and the runner already
|
|
426
|
+
// builds a session service. Only the request was being dropped.
|
|
427
|
+
test('exposes http on the wire so session middleware can read it', async () => {
|
|
428
|
+
pikkuState(null, 'mcp', 'toolsMeta').whoami = {
|
|
429
|
+
name: 'whoami',
|
|
430
|
+
title: 'Who am I',
|
|
431
|
+
description: 'Who am I',
|
|
432
|
+
pikkuFuncId: 'whoamiFunc',
|
|
433
|
+
inputSchema: null,
|
|
434
|
+
outputSchema: 'MCPToolResponse',
|
|
435
|
+
} as never
|
|
436
|
+
|
|
437
|
+
let seenAuthorization: string | undefined
|
|
438
|
+
registerFunction('whoamiFunc', async (_services, _data, wire) => {
|
|
439
|
+
seenAuthorization = wire.http?.request?.header('authorization')
|
|
440
|
+
return [{ type: 'text', text: seenAuthorization ?? 'anonymous' }]
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
const http = {
|
|
444
|
+
request: {
|
|
445
|
+
header: (name: string) =>
|
|
446
|
+
name.toLowerCase() === 'authorization'
|
|
447
|
+
? 'Bearer token-abc'
|
|
448
|
+
: undefined,
|
|
449
|
+
},
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const response = await runMCPTool(
|
|
453
|
+
{ jsonrpc: '2.0', id: 'req-http', params: {} },
|
|
454
|
+
{ mcp: mcpWire as never, http: http as never },
|
|
455
|
+
'whoami'
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
assert.equal(seenAuthorization, 'Bearer token-abc')
|
|
459
|
+
assert.deepEqual(response, {
|
|
460
|
+
id: 'req-http',
|
|
461
|
+
result: [{ type: 'text', text: 'Bearer token-abc' }],
|
|
462
|
+
})
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
test('omitting http leaves the wire without one rather than inventing it', async () => {
|
|
466
|
+
pikkuState(null, 'mcp', 'toolsMeta').anon = {
|
|
467
|
+
name: 'anon',
|
|
468
|
+
title: 'Anon',
|
|
469
|
+
description: 'Anon',
|
|
470
|
+
pikkuFuncId: 'anonFunc',
|
|
471
|
+
inputSchema: null,
|
|
472
|
+
outputSchema: 'MCPToolResponse',
|
|
473
|
+
} as never
|
|
474
|
+
|
|
475
|
+
let sawHttp: unknown = 'unset'
|
|
476
|
+
registerFunction('anonFunc', async (_services, _data, wire) => {
|
|
477
|
+
sawHttp = wire.http
|
|
478
|
+
return [{ type: 'text', text: 'ok' }]
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
await runMCPTool(
|
|
482
|
+
{ jsonrpc: '2.0', id: 'req-anon', params: {} },
|
|
483
|
+
{ mcp: mcpWire as never },
|
|
484
|
+
'anon'
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
assert.equal(sawHttp, undefined)
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
describe('MCP tools fronting a session-requiring function', () => {
|
|
492
|
+
// The whole point of carrying the request: this is the template failure,
|
|
493
|
+
// reproduced. `listRoomsTool` and five others front a `pikkuFunc`, which
|
|
494
|
+
// requires a session, and answered 'Authentication required' to every caller
|
|
495
|
+
// because no request ever reached the middleware that would have set one.
|
|
496
|
+
test('receive the session set by global middleware from the request', async () => {
|
|
497
|
+
addGlobalMiddleware([
|
|
498
|
+
async (_services, wire, next) => {
|
|
499
|
+
const header = wire.http?.request?.header('authorization')
|
|
500
|
+
if (header === 'Bearer token-abc') {
|
|
501
|
+
wire.setSession?.({ userId: 'usr_1', name: 'Rafa' } as never)
|
|
502
|
+
}
|
|
503
|
+
await next()
|
|
504
|
+
},
|
|
505
|
+
] as never)
|
|
506
|
+
|
|
507
|
+
pikkuState(null, 'mcp', 'toolsMeta').listRooms = {
|
|
508
|
+
name: 'listRooms',
|
|
509
|
+
title: 'List rooms',
|
|
510
|
+
description: 'List rooms',
|
|
511
|
+
pikkuFuncId: 'listRoomsFunc',
|
|
512
|
+
inputSchema: null,
|
|
513
|
+
outputSchema: 'MCPToolResponse',
|
|
514
|
+
} as never
|
|
515
|
+
|
|
516
|
+
// sessionless: false — the distinction that made these tools fail.
|
|
517
|
+
addFunction(
|
|
518
|
+
'listRoomsFunc',
|
|
519
|
+
{
|
|
520
|
+
func: async (_services: any, _data: any, wire: any) => [
|
|
521
|
+
{ type: 'text', text: wire.session?.userId ?? 'anonymous' },
|
|
522
|
+
],
|
|
523
|
+
} as never,
|
|
524
|
+
null
|
|
525
|
+
)
|
|
526
|
+
pikkuState(null, 'function', 'meta').listRoomsFunc = {
|
|
527
|
+
name: 'listRoomsFunc',
|
|
528
|
+
sessionless: false,
|
|
529
|
+
permissions: [],
|
|
530
|
+
} as never
|
|
531
|
+
|
|
532
|
+
const response = await runMCPTool(
|
|
533
|
+
{ jsonrpc: '2.0', id: 'req-session', params: {} },
|
|
534
|
+
{
|
|
535
|
+
mcp: mcpWire as never,
|
|
536
|
+
http: {
|
|
537
|
+
request: {
|
|
538
|
+
header: (name: string) =>
|
|
539
|
+
name.toLowerCase() === 'authorization'
|
|
540
|
+
? 'Bearer token-abc'
|
|
541
|
+
: undefined,
|
|
542
|
+
},
|
|
543
|
+
} as never,
|
|
544
|
+
},
|
|
545
|
+
'listRooms'
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
assert.deepEqual(response, {
|
|
549
|
+
id: 'req-session',
|
|
550
|
+
result: [{ type: 'text', text: 'usr_1' }],
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
})
|
|
554
|
+
|
|
374
555
|
describe('runMCPPrompt', () => {
|
|
375
556
|
test('maps missing prompt registration to not found MCP errors', async () => {
|
|
376
557
|
pikkuState(null, 'mcp', 'promptsMeta').missingPrompt = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PikkuRawWire } from '../../types/core.types.js'
|
|
2
|
+
import type { PikkuHTTP } from '../http/http.types.js'
|
|
2
3
|
import type {
|
|
3
4
|
CoreMCPResource,
|
|
4
5
|
CoreMCPPrompt,
|
|
@@ -35,6 +36,20 @@ export class MCPError extends Error {
|
|
|
35
36
|
|
|
36
37
|
export type RunMCPEndpointParams<Tools extends string = any> = {
|
|
37
38
|
mcp?: PikkuMCP<Tools>
|
|
39
|
+
/**
|
|
40
|
+
* The HTTP request the MCP call arrived on, when it arrived over HTTP.
|
|
41
|
+
*
|
|
42
|
+
* Auth middleware reads the session off `wire.http.request` — every
|
|
43
|
+
* implementation opens with `if (!http?.request) return` — so without this an
|
|
44
|
+
* MCP call reaches the function unauthenticated no matter what middleware the
|
|
45
|
+
* app has registered, and a tool fronting a session-requiring function can
|
|
46
|
+
* only ever answer 'Authentication required'.
|
|
47
|
+
*
|
|
48
|
+
* Left undefined for transports that have no request to offer, such as stdio.
|
|
49
|
+
* Those remain anonymous, which is a property of the transport rather than a
|
|
50
|
+
* default chosen here.
|
|
51
|
+
*/
|
|
52
|
+
http?: PikkuHTTP
|
|
38
53
|
/** Defaults to enabled outside production. */
|
|
39
54
|
exposeErrors?: boolean
|
|
40
55
|
}
|
|
@@ -97,6 +112,12 @@ export async function runMCPResource(
|
|
|
97
112
|
const metas = pikkuState(null, 'mcp', 'resourcesMeta')
|
|
98
113
|
const endpoints = pikkuState(null, 'mcp', 'resources')
|
|
99
114
|
|
|
115
|
+
// The key the resource's meta (and its middleware) is stored under. For a
|
|
116
|
+
// templated resource this is the template, not the concrete request URI — so
|
|
117
|
+
// it must be carried forward, or the meta lookup downstream misses and the
|
|
118
|
+
// resource's declared middleware (including auth) is silently skipped.
|
|
119
|
+
let metaKey = uri
|
|
120
|
+
|
|
100
121
|
if (endpoints.has(uri)) {
|
|
101
122
|
endpoint = endpoints.get(uri)
|
|
102
123
|
pikkuFuncId = metas[uri]?.pikkuFuncId
|
|
@@ -113,6 +134,7 @@ export async function runMCPResource(
|
|
|
113
134
|
|
|
114
135
|
if (match) {
|
|
115
136
|
endpoint = value
|
|
137
|
+
metaKey = uriTemplate
|
|
116
138
|
pikkuFuncId = metas[uriTemplate]?.pikkuFuncId
|
|
117
139
|
|
|
118
140
|
for (let i = 0; i < paramNames.length; i++) {
|
|
@@ -134,7 +156,8 @@ export async function runMCPResource(
|
|
|
134
156
|
pikkuFuncId,
|
|
135
157
|
{ ...params, mcp: { ...params.mcp, uri } } as RunMCPEndpointParams<
|
|
136
158
|
keyof CoreMCPResource
|
|
137
|
-
|
|
159
|
+
>,
|
|
160
|
+
metaKey
|
|
138
161
|
)
|
|
139
162
|
}
|
|
140
163
|
|
|
@@ -177,7 +200,11 @@ async function runMCPPikkuFunc(
|
|
|
177
200
|
name: string,
|
|
178
201
|
mcp: CoreMCPResource | CoreMCPPrompt | undefined,
|
|
179
202
|
pikkuFuncId: string | undefined,
|
|
180
|
-
{ mcp: mcpWire, exposeErrors = !isProduction() }: RunMCPEndpointParams
|
|
203
|
+
{ mcp: mcpWire, http, exposeErrors = !isProduction() }: RunMCPEndpointParams,
|
|
204
|
+
// The key the endpoint's meta is stored under. Differs from `name` only for a
|
|
205
|
+
// templated resource, where `name` is the concrete URI and the meta lives
|
|
206
|
+
// under the template. Defaults to `name` for tools and prompts.
|
|
207
|
+
metaKey: string = name
|
|
181
208
|
): Promise<JsonRpcResponse> {
|
|
182
209
|
const singletonServices = getSingletonServices()
|
|
183
210
|
const createWireServices = getCreateWireServices()
|
|
@@ -208,16 +235,19 @@ async function runMCPPikkuFunc(
|
|
|
208
235
|
)
|
|
209
236
|
const wire: PikkuRawWire = {
|
|
210
237
|
mcp: mcpWire,
|
|
238
|
+
// Only when the transport supplied one. Setting `http: undefined`
|
|
239
|
+
// explicitly would be the same to a reader and different to a spread.
|
|
240
|
+
...(http ? { http } : {}),
|
|
211
241
|
...createMiddlewareSessionWireProps(mcpSessionService),
|
|
212
242
|
}
|
|
213
243
|
|
|
214
244
|
let meta: any
|
|
215
245
|
if (type === 'resource') {
|
|
216
|
-
meta = pikkuState(null, 'mcp', 'resourcesMeta')[
|
|
246
|
+
meta = pikkuState(null, 'mcp', 'resourcesMeta')[metaKey]
|
|
217
247
|
} else if (type === 'tool') {
|
|
218
|
-
meta = pikkuState(null, 'mcp', 'toolsMeta')[
|
|
248
|
+
meta = pikkuState(null, 'mcp', 'toolsMeta')[metaKey]
|
|
219
249
|
} else if (type === 'prompt') {
|
|
220
|
-
meta = pikkuState(null, 'mcp', 'promptsMeta')[
|
|
250
|
+
meta = pikkuState(null, 'mcp', 'promptsMeta')[metaKey]
|
|
221
251
|
}
|
|
222
252
|
|
|
223
253
|
let resolvedFuncName = pikkuFuncId
|
|
@@ -10,7 +10,6 @@ export type { RoleVerification } from './validate-personas.js'
|
|
|
10
10
|
export {
|
|
11
11
|
personaEnvironmentErrors,
|
|
12
12
|
personaEnvironmentRefusal,
|
|
13
|
-
resolvePersonaEnvironments,
|
|
14
13
|
} from './persona-environments.js'
|
|
15
14
|
export type {
|
|
16
15
|
PersonaEnvironment,
|
|
@@ -18,14 +17,8 @@ export type {
|
|
|
18
17
|
} from './persona-environments.js'
|
|
19
18
|
export {
|
|
20
19
|
personaEmail,
|
|
21
|
-
personaEmailLabel,
|
|
22
20
|
personaEmails,
|
|
23
21
|
} from './persona-email.js'
|
|
24
|
-
export {
|
|
25
|
-
allowedLinks,
|
|
26
|
-
applyMailboxAllowlist,
|
|
27
|
-
isAllowedSender,
|
|
28
|
-
} from './persona-mailbox.js'
|
|
29
22
|
export type {
|
|
30
23
|
MailboxAllowlist,
|
|
31
24
|
PersonaMailbox,
|
|
@@ -33,7 +26,6 @@ export type {
|
|
|
33
26
|
} from './persona-mailbox.js'
|
|
34
27
|
export type {
|
|
35
28
|
CorePersona,
|
|
36
|
-
CorePersonaAccount,
|
|
37
29
|
CorePersonas,
|
|
38
30
|
PersonaAccountMeta,
|
|
39
31
|
PersonaDefinitions,
|
|
@@ -17,26 +17,12 @@ export type {
|
|
|
17
17
|
export {
|
|
18
18
|
wireQueueWorker,
|
|
19
19
|
runQueueJob,
|
|
20
|
-
getQueueWorkers,
|
|
21
|
-
removeQueueWorker,
|
|
22
20
|
QueueJobDiscardedError,
|
|
23
21
|
QueueJobFailedError,
|
|
24
22
|
} from './queue-runner.js'
|
|
25
|
-
|
|
26
|
-
export {
|
|
27
|
-
QUEUE_IDENTITY_CLAIM_VERSION,
|
|
28
|
-
QUEUE_IDENTITY_INFO,
|
|
29
|
-
QUEUE_IDENTITY_SECRET_NAME,
|
|
30
|
-
getQueueIdentitySecret,
|
|
31
|
-
resolveQueueJobIdentity,
|
|
32
|
-
signQueueIdentity,
|
|
33
|
-
verifyQueueIdentity,
|
|
34
|
-
} from './queue-identity.js'
|
|
35
23
|
export type { QueueIdentityBinding } from './queue-identity.js'
|
|
36
24
|
|
|
37
25
|
export { SignedQueueService } from './signed-queue-service.js'
|
|
38
|
-
|
|
39
|
-
export { validateWorkerConfig } from './validate-worker-config.js'
|
|
40
26
|
export type { QueueConfigMapping } from './validate-worker-config.js'
|
|
41
27
|
|
|
42
28
|
export { registerQueueWorkers } from './register-queue-helper.js'
|