okengine 0.21.1 → 0.23.0
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/AGENTS.md +11 -4
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +15 -9
- package/manifest.v1.schema.json +54 -0
- package/package.json +53 -38
- package/site/content/docs/ai/mcp.mdx +13 -9
- package/site/content/docs/ai/skills.mdx +2 -2
- package/site/content/docs/client/calling.mdx +27 -19
- package/site/content/docs/client/index.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/channel/email.mdx +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/flow/http.mdx +4 -4
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/meta.json +1 -1
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/elements/vault/rotation.mdx +15 -0
- package/site/content/docs/plugins/otp.mdx +31 -31
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +1 -1
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +11 -9
- package/site/content/docs/reference/errors.mdx +12 -2
- package/site/content/docs/reference/fx.mdx +35 -21
- package/site/content/docs/reference/idempotency.mdx +192 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +2 -1
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
- package/site/content/docs/understand/the-architecture.mdx +14 -12
- package/site/content/docs/understand/try-it.mdx +3 -3
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +14 -1
- package/src/cli/load-config.images.test.ts +12 -12
- package/src/cli/load-config.ts +2 -2
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/cli/vault-cmd.test.ts +23 -0
- package/src/cli/vault-cmd.ts +7 -0
- package/src/client/agent.ts +307 -0
- package/src/client/create.ts +18 -1
- package/src/client/transport.test.ts +245 -0
- package/src/client/transport.ts +133 -19
- package/src/client/types.ts +29 -3
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-infer.ts +753 -17
- package/src/compiler/extract.test.ts +37 -0
- package/src/compiler/extract.ts +381 -24
- package/src/compiler/fixtures/skyport.expected.json +24 -0
- package/src/compiler/fx-follow.test.ts +164 -0
- package/src/compiler/fx-index.ts +399 -0
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/docker.test.ts +13 -13
- package/src/docker/images-config.test.ts +12 -12
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/docker/stack-id.test.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.ts +455 -3
- package/src/drivers/meilisearch.ts +2 -0
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/drivers/vault-builtin.test.ts +10 -0
- package/src/drivers/vault-builtin.ts +13 -1
- package/src/drivers/vault-remote-bag.ts +5 -1
- package/src/drivers/vault-types.ts +6 -1
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1186 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/store/emit-drizzle.ts +2 -2
- package/src/elements/store/live-default.test.ts +2 -0
- package/src/elements/store/live-http.test.ts +1 -0
- package/src/elements/store/resource-list-docs.test.ts +2 -2
- package/src/elements/store/resource.test.ts +3 -3
- package/src/elements/store/schema-decl.test.ts +1 -0
- package/src/elements/store/sql-condition.ts +24 -7
- package/src/elements/store/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/vault/audit.test.ts +137 -0
- package/src/elements/vault/audit.ts +106 -13
- package/src/elements/vault/boot-chain.ts +9 -1
- package/src/elements/vault/builtin-adapter.ts +33 -3
- package/src/i18n/catalogs/ar.ts +9 -0
- package/src/i18n/catalogs/en.ts +9 -0
- package/src/index.ts +2 -0
- package/src/kernel/abort-scope.ts +33 -2
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +294 -19
- package/src/kernel/auto-cache.test.ts +6 -6
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot-bind/vault.ts +1 -0
- package/src/kernel/boot.ts +34 -5
- package/src/kernel/builtin-errors.ts +10 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/client-descriptor.ts +1 -0
- package/src/kernel/concurrency.test.ts +33 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects-stamping.test.ts +2 -2
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-compiler.ts +20 -0
- package/src/kernel/errors-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/external-effects.test.ts +36 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +24 -0
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx-fetch.ts +5 -1
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +316 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency-store.ts +578 -0
- package/src/kernel/idempotency.test.ts +556 -0
- package/src/kernel/idempotency.ts +304 -0
- package/src/kernel/index.ts +1 -0
- package/src/kernel/journal.ts +160 -0
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +37 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/otp.test.ts +65 -1
- package/src/plugins/otp.ts +59 -7
- package/src/release/absolute-regression.test.ts +63 -0
- package/src/release/http-graph.test.ts +27 -0
- package/src/release/http-graph.ts +95 -0
- package/src/release/index.ts +3 -0
- package/src/release/limits.ts +17 -2
- package/src/release/measure.ts +144 -28
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/test/create-test-app.test.ts +3 -1
- package/src/test/create-test-app.ts +40 -0
- package/src/test/live-signals.test.ts +1 -0
- package/src/test/provisions.integration.test.ts +1 -0
- package/src/test/tenant-isolation.test.ts +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
- package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
package/src/manifest/types.ts
CHANGED
|
@@ -58,6 +58,12 @@ export type AuthApiKeysResourceRef = "auth:api-keys";
|
|
|
58
58
|
*/
|
|
59
59
|
export type AuthTenantsResourceRef = "auth:tenants";
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Agent-run journal capability for approval routes.
|
|
63
|
+
* Not a store facet — the journal driver owns the rows.
|
|
64
|
+
*/
|
|
65
|
+
export type JournalRunsResourceRef = "journal:runs";
|
|
66
|
+
|
|
61
67
|
/**
|
|
62
68
|
* Dead-letter / live-stream read capability — not a store facet.
|
|
63
69
|
* Declare on `effects.reads` (never `writes`).
|
|
@@ -101,8 +107,10 @@ export interface Effects {
|
|
|
101
107
|
| AuthApiKeysResourceRef
|
|
102
108
|
| AuthTenantsResourceRef
|
|
103
109
|
>;
|
|
104
|
-
/** Store writes, plus `"auth:api-keys"` / `"auth:tenants"`
|
|
105
|
-
writes?: Array<
|
|
110
|
+
/** Store writes, plus `"auth:api-keys"` / `"auth:tenants"` and `"journal:runs"`. */
|
|
111
|
+
writes?: Array<
|
|
112
|
+
ResourceRef | AuthApiKeysResourceRef | AuthTenantsResourceRef | JournalRunsResourceRef
|
|
113
|
+
>;
|
|
106
114
|
/** Emitted signals. */
|
|
107
115
|
emits?: SignalRef[];
|
|
108
116
|
/** Channel template sends (irreversible). */
|
|
@@ -123,6 +131,8 @@ export interface Effects {
|
|
|
123
131
|
* Hostname only (e.g. `"api.stripe.com"`), never a full URL.
|
|
124
132
|
*/
|
|
125
133
|
fetches?: FetchHostRef[];
|
|
134
|
+
/** Decision refs from `fx.decide` (irreversible, nondeterministic). */
|
|
135
|
+
decides?: string[];
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
/** Embedding model ref for `effects.embeds` / `fx.embed` (model name). */
|
|
@@ -201,6 +211,14 @@ export interface Flow {
|
|
|
201
211
|
source?: string;
|
|
202
212
|
plane?: FlowPlane;
|
|
203
213
|
durable?: boolean;
|
|
214
|
+
/**
|
|
215
|
+
* HTTP idempotency stamp. `off` ignores `Idempotency-Key`.
|
|
216
|
+
* `auto` honors it when present. `required` rejects a missing header.
|
|
217
|
+
* `ttl` is how long a stored response (possibly PII) is kept.
|
|
218
|
+
*/
|
|
219
|
+
idempotency?: { mode: "off" | "auto" | "required"; ttl: string };
|
|
220
|
+
/** True when the body calls `fx.raw`. Throws then keep the idempotency record. */
|
|
221
|
+
usesRaw?: boolean;
|
|
204
222
|
/** Live signal name when this flow streams `delivery: "live"` SSE. */
|
|
205
223
|
live?: string;
|
|
206
224
|
cache?: boolean | string;
|
|
@@ -467,6 +485,8 @@ export interface AiPrompt {
|
|
|
467
485
|
version?: number;
|
|
468
486
|
evals?: string;
|
|
469
487
|
budget?: AiBudget;
|
|
488
|
+
/** `1` sends one schema-mismatch follow-up. Default `0`. */
|
|
489
|
+
repair?: 0 | 1;
|
|
470
490
|
/** Ordered recovery chain of logical model names. */
|
|
471
491
|
via?: string[];
|
|
472
492
|
/** Per-command deadline (`"30s"` or milliseconds). */
|
|
@@ -479,7 +499,10 @@ export interface AiPrompt {
|
|
|
479
499
|
/** Bounded agent whose tools are flows. */
|
|
480
500
|
export interface AiAgent {
|
|
481
501
|
tools?: FlowRef[];
|
|
502
|
+
/** Tools that park for human approval. The predicate stays in author code. */
|
|
503
|
+
approvals?: Record<string, true>;
|
|
482
504
|
maxSteps?: number;
|
|
505
|
+
maxDepth?: number;
|
|
483
506
|
model?: string;
|
|
484
507
|
budget?: AiBudget;
|
|
485
508
|
}
|
|
@@ -506,9 +529,21 @@ export interface Ai {
|
|
|
506
529
|
models?: Record<string, AiModel>;
|
|
507
530
|
prompts?: Record<string, AiPrompt>;
|
|
508
531
|
agents?: Record<string, AiAgent>;
|
|
532
|
+
decisions?: Record<string, AiDecision>;
|
|
509
533
|
mcpServers?: Record<string, AiMcpServer>;
|
|
510
534
|
}
|
|
511
535
|
|
|
536
|
+
/** A declared decision. The predicate and locale function stay in author code. */
|
|
537
|
+
export interface AiDecision {
|
|
538
|
+
mode: "review" | "abstain";
|
|
539
|
+
review?: string;
|
|
540
|
+
model?: string;
|
|
541
|
+
driverId?: "openrouter" | "typesafe";
|
|
542
|
+
questions: string[];
|
|
543
|
+
evals?: string;
|
|
544
|
+
autonomy?: { maxError: number; audit: number; risk?: number };
|
|
545
|
+
}
|
|
546
|
+
|
|
512
547
|
/** Per-table metadata on a Manifest {@link Plugin}. */
|
|
513
548
|
export interface PluginTable {
|
|
514
549
|
plane?: string;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only AI and decision MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* Same authorization shape as `oke.traces.*`: one of the declared scopes,
|
|
5
|
+
* no confirmation, and no resolve or promote tool.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import { createSessionStore } from "../auth/sessions.ts";
|
|
10
|
+
import { Redacted } from "../kernel/redacted.ts";
|
|
11
|
+
import type { Manifest } from "../manifest/types.ts";
|
|
12
|
+
import { authorizeToolCall } from "./authorization.ts";
|
|
13
|
+
import { mintMcpSession, authenticateMcpRequest } from "./session.ts";
|
|
14
|
+
import { createToolRuntime, type McpContext } from "./tools.ts";
|
|
15
|
+
|
|
16
|
+
const SECRET = "mcp-ai-secret";
|
|
17
|
+
|
|
18
|
+
const runA = {
|
|
19
|
+
id: "run-a",
|
|
20
|
+
agent: "support",
|
|
21
|
+
tenant: "acme",
|
|
22
|
+
status: "interrupted",
|
|
23
|
+
};
|
|
24
|
+
const runB = {
|
|
25
|
+
id: "run-b",
|
|
26
|
+
agent: "support",
|
|
27
|
+
tenant: "other",
|
|
28
|
+
status: "finished",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("MCP AI and decision tools", () => {
|
|
32
|
+
test("traces scope cannot read agent runs, and there is no resolve tool", () => {
|
|
33
|
+
expect(
|
|
34
|
+
authorizeToolCall("oke.ai.runs.list", {}, ["mcp:traces:read"], { confirmed: true }).ok,
|
|
35
|
+
).toBe(false);
|
|
36
|
+
const resolve = authorizeToolCall("oke.ai.approvals.resolve", {}, ["console:*"]);
|
|
37
|
+
expect(resolve.ok).toBe(false);
|
|
38
|
+
if (resolve.ok) return;
|
|
39
|
+
expect(resolve.reason).toBe("unknown-tool");
|
|
40
|
+
const promote = authorizeToolCall("oke.decisions.promote", {}, ["mcp:*"]);
|
|
41
|
+
expect(promote.ok).toBe(false);
|
|
42
|
+
if (promote.ok) return;
|
|
43
|
+
expect(promote.reason).toBe("unknown-tool");
|
|
44
|
+
expect(
|
|
45
|
+
authorizeToolCall("oke.ai.runs.list", {}, ["console:runs:read"], { confirmed: true }).ok,
|
|
46
|
+
).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("a tenant token sees only its tenant; an operator token sees every tenant", async () => {
|
|
50
|
+
const runtime = createToolRuntime({
|
|
51
|
+
getManifest: () => ({ oke: "1.0", app: "skyport" }) as Manifest,
|
|
52
|
+
listRuns: async () => [],
|
|
53
|
+
listAgentRuns: async () => [runA, runB],
|
|
54
|
+
getAgentRun: async (runId) => {
|
|
55
|
+
const run = [runA, runB].find((row) => row.id === runId);
|
|
56
|
+
if (!run) return undefined;
|
|
57
|
+
return { run, events: [{ seq: 1, event: { type: "RUN_FINISHED" } }] };
|
|
58
|
+
},
|
|
59
|
+
listApprovals: async () => [
|
|
60
|
+
{ id: "a", tenant: "acme", tool: "refund" },
|
|
61
|
+
{ id: "b", tenant: "other", tool: "refund" },
|
|
62
|
+
],
|
|
63
|
+
listDecisions: async () => [
|
|
64
|
+
{ name: "triage", tenant: "acme", state: "learning", pending: 1, drift: false },
|
|
65
|
+
{ name: "other-call", tenant: "other", state: "learning", pending: 0, drift: false },
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
const store = createSessionStore();
|
|
69
|
+
const issued = await mintMcpSession({
|
|
70
|
+
store,
|
|
71
|
+
secret: SECRET,
|
|
72
|
+
principalId: "op",
|
|
73
|
+
scopes: ["mcp:ai:read", "mcp:decisions:read"],
|
|
74
|
+
tenantId: "acme",
|
|
75
|
+
});
|
|
76
|
+
const requester = await authenticateMcpRequest(store, SECRET, issued.accessToken);
|
|
77
|
+
|
|
78
|
+
const listed = await runtime.callTool(requester, "oke.ai.runs.list", {});
|
|
79
|
+
expect(listed.ok).toBe(true);
|
|
80
|
+
if (!listed.ok) return;
|
|
81
|
+
const runs = (listed.data.content as { runs: { id: string }[] }).runs;
|
|
82
|
+
expect(runs.map((row) => row.id)).toEqual(["run-a"]);
|
|
83
|
+
|
|
84
|
+
const foreign = await runtime.callTool(requester, "oke.ai.runs.get", { runId: "run-b" });
|
|
85
|
+
expect(foreign.ok).toBe(false);
|
|
86
|
+
const namedOther = await runtime.callTool(requester, "oke.ai.runs.list", { tenant: "other" });
|
|
87
|
+
expect(namedOther.ok).toBe(true);
|
|
88
|
+
if (!namedOther.ok) return;
|
|
89
|
+
expect((namedOther.data.content as { runs: unknown[] }).runs).toEqual([]);
|
|
90
|
+
expect(foreign.ok).toBe(false);
|
|
91
|
+
|
|
92
|
+
const got = await runtime.callTool(requester, "oke.ai.runs.get", {
|
|
93
|
+
runId: "run-a",
|
|
94
|
+
tenant: "acme",
|
|
95
|
+
});
|
|
96
|
+
expect(got.ok).toBe(true);
|
|
97
|
+
if (!got.ok) return;
|
|
98
|
+
expect((got.data.content as { events: { seq: number }[] }).events[0]?.seq).toBe(1);
|
|
99
|
+
|
|
100
|
+
const approvals = await runtime.callTool(requester, "oke.ai.approvals.list", {
|
|
101
|
+
tenant: "acme",
|
|
102
|
+
});
|
|
103
|
+
expect(approvals.ok).toBe(true);
|
|
104
|
+
if (!approvals.ok) return;
|
|
105
|
+
expect(
|
|
106
|
+
(approvals.data.content as { rows: { id: string }[] }).rows.map((row) => row.id),
|
|
107
|
+
).toEqual(["a"]);
|
|
108
|
+
|
|
109
|
+
const decisions = await runtime.callTool(requester, "oke.decisions.list", {});
|
|
110
|
+
expect(decisions.ok).toBe(true);
|
|
111
|
+
if (!decisions.ok) return;
|
|
112
|
+
expect(
|
|
113
|
+
(decisions.data.content as { decisions: { name: string }[] }).decisions.map(
|
|
114
|
+
(row) => row.name,
|
|
115
|
+
),
|
|
116
|
+
).toEqual(["triage"]);
|
|
117
|
+
|
|
118
|
+
const operatorIssued = await mintMcpSession({
|
|
119
|
+
store,
|
|
120
|
+
secret: SECRET,
|
|
121
|
+
principalId: "root",
|
|
122
|
+
scopes: ["mcp:*"],
|
|
123
|
+
});
|
|
124
|
+
const operator = await authenticateMcpRequest(store, SECRET, operatorIssued.accessToken);
|
|
125
|
+
const allRuns = await runtime.callTool(operator, "oke.ai.runs.list", {});
|
|
126
|
+
expect(allRuns.ok).toBe(true);
|
|
127
|
+
if (!allRuns.ok) return;
|
|
128
|
+
expect((allRuns.data.content as { runs: { id: string }[] }).runs.map((row) => row.id)).toEqual([
|
|
129
|
+
"run-a",
|
|
130
|
+
"run-b",
|
|
131
|
+
]);
|
|
132
|
+
const otherApprovals = await runtime.callTool(operator, "oke.ai.approvals.list", {
|
|
133
|
+
tenant: "other",
|
|
134
|
+
});
|
|
135
|
+
expect(otherApprovals.ok).toBe(true);
|
|
136
|
+
if (!otherApprovals.ok) return;
|
|
137
|
+
expect(
|
|
138
|
+
(otherApprovals.data.content as { rows: { id: string }[] }).rows.map((row) => row.id),
|
|
139
|
+
).toEqual(["b"]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("every AI tool masks PII fields and redacted secrets", async () => {
|
|
143
|
+
const secret = Redacted.of("sk-live");
|
|
144
|
+
const manifest = {
|
|
145
|
+
oke: "1.0",
|
|
146
|
+
app: "skyport",
|
|
147
|
+
stores: {
|
|
148
|
+
app: {
|
|
149
|
+
tables: {
|
|
150
|
+
people: { columns: { email: { pii: true }, apiKey: { sensitive: true } } },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
} as unknown as Manifest;
|
|
155
|
+
const ctx: McpContext = {
|
|
156
|
+
getManifest: () => manifest,
|
|
157
|
+
listRuns: async () => [],
|
|
158
|
+
listAgentRuns: async () => [
|
|
159
|
+
{
|
|
160
|
+
id: "run-a",
|
|
161
|
+
tenant: "acme",
|
|
162
|
+
args: { email: "a@b.co", apiKey: secret },
|
|
163
|
+
trail: [{ args: { email: "a@b.co" } }],
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
getAgentRun: async () => ({
|
|
167
|
+
run: { id: "run-a", tenant: "acme", args: { email: "a@b.co", apiKey: secret } },
|
|
168
|
+
events: [{ seq: 1, event: { type: "CUSTOM", value: { email: "a@b.co", apiKey: secret } } }],
|
|
169
|
+
}),
|
|
170
|
+
listApprovals: async () => [
|
|
171
|
+
{ id: "a", tenant: "acme", args: { email: "a@b.co", apiKey: secret } },
|
|
172
|
+
],
|
|
173
|
+
listDecisions: async () => [
|
|
174
|
+
{ name: "triage", tenant: "acme", input: { email: "a@b.co", apiKey: secret } },
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
const runtime = createToolRuntime(ctx);
|
|
178
|
+
const store = createSessionStore();
|
|
179
|
+
const issued = await mintMcpSession({
|
|
180
|
+
store,
|
|
181
|
+
secret: SECRET,
|
|
182
|
+
principalId: "op",
|
|
183
|
+
scopes: ["mcp:ai:read", "mcp:decisions:read"],
|
|
184
|
+
tenantId: "acme",
|
|
185
|
+
});
|
|
186
|
+
const requester = await authenticateMcpRequest(store, SECRET, issued.accessToken);
|
|
187
|
+
const blobs = [
|
|
188
|
+
await runtime.callTool(requester, "oke.ai.runs.list", {}),
|
|
189
|
+
await runtime.callTool(requester, "oke.ai.runs.get", { runId: "run-a" }),
|
|
190
|
+
await runtime.callTool(requester, "oke.ai.approvals.list", {}),
|
|
191
|
+
await runtime.callTool(requester, "oke.decisions.list", {}),
|
|
192
|
+
];
|
|
193
|
+
for (const result of blobs) {
|
|
194
|
+
expect(result.ok).toBe(true);
|
|
195
|
+
if (!result.ok) continue;
|
|
196
|
+
const text = JSON.stringify(result.data.content);
|
|
197
|
+
expect(text).not.toContain("a@b.co");
|
|
198
|
+
expect(text).not.toContain("sk-live");
|
|
199
|
+
expect(text).toContain("[redacted]");
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
package/src/mcp/authorization.ts
CHANGED
|
@@ -133,6 +133,72 @@ export const MCP_TOOL_POLICIES: readonly McpToolPolicy[] = [
|
|
|
133
133
|
},
|
|
134
134
|
params: [{ name: "runId", maxLength: 128 }],
|
|
135
135
|
},
|
|
136
|
+
{
|
|
137
|
+
name: "oke.ai.runs.list",
|
|
138
|
+
scopes: ["mcp:ai:read", "console:runs:read"],
|
|
139
|
+
mutability: "read",
|
|
140
|
+
description: "List agent runs as inert data. Approvals stay in the Console.",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {
|
|
144
|
+
limit: { type: "integer", minimum: 1, maximum: 200 },
|
|
145
|
+
tenant: { type: "string" },
|
|
146
|
+
},
|
|
147
|
+
additionalProperties: false,
|
|
148
|
+
},
|
|
149
|
+
params: [
|
|
150
|
+
{ name: "limit", maxLength: 3 },
|
|
151
|
+
{ name: "tenant", maxLength: 128 },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "oke.ai.runs.get",
|
|
156
|
+
scopes: ["mcp:ai:read", "console:runs:read"],
|
|
157
|
+
mutability: "read",
|
|
158
|
+
description: "Return one agent run record and its follow events as inert data.",
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: "object",
|
|
161
|
+
properties: {
|
|
162
|
+
runId: { type: "string", minLength: 1 },
|
|
163
|
+
tenant: { type: "string" },
|
|
164
|
+
},
|
|
165
|
+
required: ["runId"],
|
|
166
|
+
additionalProperties: false,
|
|
167
|
+
},
|
|
168
|
+
params: [
|
|
169
|
+
{ name: "runId", maxLength: 128 },
|
|
170
|
+
{ name: "tenant", maxLength: 128 },
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "oke.ai.approvals.list",
|
|
175
|
+
scopes: ["mcp:ai:read", "console:runs:read"],
|
|
176
|
+
mutability: "read",
|
|
177
|
+
description: "List pending tool approvals as inert data. Resolve stays in the Console.",
|
|
178
|
+
inputSchema: {
|
|
179
|
+
type: "object",
|
|
180
|
+
properties: {
|
|
181
|
+
tenant: { type: "string" },
|
|
182
|
+
},
|
|
183
|
+
additionalProperties: false,
|
|
184
|
+
},
|
|
185
|
+
params: [{ name: "tenant", maxLength: 128 }],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "oke.decisions.list",
|
|
189
|
+
scopes: ["mcp:decisions:read", "console:runs:read"],
|
|
190
|
+
mutability: "read",
|
|
191
|
+
description:
|
|
192
|
+
"List decisions with state, pending count, certificate metrics, and drift. Promote stays in the CLI.",
|
|
193
|
+
inputSchema: {
|
|
194
|
+
type: "object",
|
|
195
|
+
properties: {
|
|
196
|
+
tenant: { type: "string" },
|
|
197
|
+
},
|
|
198
|
+
additionalProperties: false,
|
|
199
|
+
},
|
|
200
|
+
params: [{ name: "tenant", maxLength: 128 }],
|
|
201
|
+
},
|
|
136
202
|
{
|
|
137
203
|
name: "oke.action.invoke",
|
|
138
204
|
scopes: ["mcp:action:invoke", "console:flows:invoke"],
|
package/src/mcp/session.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface MintMcpSessionOptions {
|
|
|
38
38
|
readonly secret: string;
|
|
39
39
|
readonly principalId: string;
|
|
40
40
|
readonly scopes: Iterable<string>;
|
|
41
|
+
/** Tenant stamped on the access token. Omitted tokens have no tenant. */
|
|
42
|
+
readonly tenantId?: string | null;
|
|
41
43
|
readonly now?: () => number;
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -58,6 +60,7 @@ export async function mintMcpSession(options: MintMcpSessionOptions): Promise<Is
|
|
|
58
60
|
id: options.principalId,
|
|
59
61
|
plane: "operator",
|
|
60
62
|
scopes: options.scopes,
|
|
63
|
+
...(options.tenantId !== undefined ? { tenantId: options.tenantId } : {}),
|
|
61
64
|
},
|
|
62
65
|
);
|
|
63
66
|
}
|
package/src/mcp/tools.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* confirmation token (no session-level consent cache).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { lazyRequire } from "../kernel/lazy-require.ts";
|
|
8
9
|
import type { Manifest } from "../manifest/types.ts";
|
|
9
10
|
import type { WideEvent } from "../runs/types.ts";
|
|
10
11
|
import { authorizeToolCall, MCP_TOOL_POLICIES, type McpToolPolicy } from "./authorization.ts";
|
|
@@ -22,6 +23,16 @@ export interface McpContext {
|
|
|
22
23
|
readonly getManifest: () => Manifest | null;
|
|
23
24
|
/** List runs/traces (already operator-scoped by the provider). */
|
|
24
25
|
readonly listRuns: () => Promise<readonly WideEvent[]>;
|
|
26
|
+
/** Agent runs for `oke.ai.runs.*`. Absent providers return an empty list. */
|
|
27
|
+
readonly listAgentRuns?: () => Promise<readonly unknown[]>;
|
|
28
|
+
/** One agent run plus follow events. */
|
|
29
|
+
readonly getAgentRun?: (
|
|
30
|
+
runId: string,
|
|
31
|
+
) => Promise<{ readonly run: unknown; readonly events: readonly unknown[] } | undefined>;
|
|
32
|
+
/** Pending tool approvals. Resolve is not an MCP tool. */
|
|
33
|
+
readonly listApprovals?: () => Promise<readonly unknown[]>;
|
|
34
|
+
/** Decision catalogue: state, pending count, metrics, drift. */
|
|
35
|
+
readonly listDecisions?: (tenant?: string) => Promise<readonly unknown[]>;
|
|
25
36
|
/**
|
|
26
37
|
* Safe invoke adapter. Called only after confirmation. Must NOT receive
|
|
27
38
|
* the caller's Bearer token — capabilities are passed as structured input.
|
|
@@ -144,6 +155,14 @@ export function createToolRuntime(
|
|
|
144
155
|
return tracesList(ctx, args);
|
|
145
156
|
case "oke.traces.get":
|
|
146
157
|
return tracesGet(ctx, args);
|
|
158
|
+
case "oke.ai.runs.list":
|
|
159
|
+
return aiRunsList(ctx, requester, args);
|
|
160
|
+
case "oke.ai.runs.get":
|
|
161
|
+
return aiRunsGet(ctx, requester, args);
|
|
162
|
+
case "oke.ai.approvals.list":
|
|
163
|
+
return aiApprovalsList(ctx, requester, args);
|
|
164
|
+
case "oke.decisions.list":
|
|
165
|
+
return decisionsList(ctx, requester, args);
|
|
147
166
|
case "oke.action.confirm":
|
|
148
167
|
return actionConfirm(confirm, requester, args);
|
|
149
168
|
case "oke.action.invoke":
|
|
@@ -243,6 +262,155 @@ async function tracesGet(ctx: McpContext, args: Record<string, unknown>): Promis
|
|
|
243
262
|
return okData(freezeData({ run: projectRun(run) }), "store-record");
|
|
244
263
|
}
|
|
245
264
|
|
|
265
|
+
function tenantArg(args: Record<string, unknown>): string | undefined {
|
|
266
|
+
return typeof args.tenant === "string" && args.tenant.length > 0 ? args.tenant : undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function sameTenant(row: unknown, tenant: string | undefined): boolean {
|
|
270
|
+
if (!tenant) return true;
|
|
271
|
+
if (!row || typeof row !== "object") return false;
|
|
272
|
+
return (row as { tenant?: unknown }).tenant === tenant;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** `console:*` and `mcp:*` may list every tenant. A named tool scope stays on its token. */
|
|
276
|
+
function operatorTenantScope(scopes: readonly string[]): boolean {
|
|
277
|
+
return scopes.includes("console:*") || scopes.includes("mcp:*");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Tenant a read is allowed to see.
|
|
282
|
+
*
|
|
283
|
+
* The token's `tid` is the default. Naming another tenant, or listing every
|
|
284
|
+
* tenant, requires an operator scope. A non-operator token with no tenant
|
|
285
|
+
* sees nothing.
|
|
286
|
+
*/
|
|
287
|
+
function visibleTenant(
|
|
288
|
+
requester: McpRequester,
|
|
289
|
+
args: Record<string, unknown>,
|
|
290
|
+
): { readonly ok: true; readonly tenant: string | undefined } | { readonly ok: false } {
|
|
291
|
+
const requested = tenantArg(args);
|
|
292
|
+
const own = requester.claims.tid;
|
|
293
|
+
if (operatorTenantScope(requester.scopes)) {
|
|
294
|
+
return { ok: true, tenant: requested };
|
|
295
|
+
}
|
|
296
|
+
if (!own) return { ok: false };
|
|
297
|
+
if (requested !== undefined && requested !== own) return { ok: false };
|
|
298
|
+
return { ok: true, tenant: own };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function loadClassify(): typeof import("../elements/store/classify.ts") {
|
|
302
|
+
return lazyRequire(`${import.meta.dir}/../elements/store`, ["class", "ify"].join(""));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function loadRedacted(): typeof import("../kernel/redacted.ts") {
|
|
306
|
+
return lazyRequire(`${import.meta.dir}/../kernel`, ["redact", "ed"].join(""));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* PII-classified and sensitive column names, plus {@link Redacted} values.
|
|
311
|
+
* Same mask token the Console traces projection uses.
|
|
312
|
+
*/
|
|
313
|
+
function maskAiValue(manifest: Manifest | null | undefined, value: unknown): unknown {
|
|
314
|
+
return maskPiiWalk(loadRedacted().maskRedactedDeep(value), classifiedFieldNames(manifest));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function classifiedFieldNames(manifest: Manifest | null | undefined): ReadonlySet<string> {
|
|
318
|
+
const names = new Set<string>();
|
|
319
|
+
if (!manifest?.stores) return names;
|
|
320
|
+
for (const store of Object.values(manifest.stores)) {
|
|
321
|
+
for (const table of Object.values(store.tables ?? {})) {
|
|
322
|
+
for (const [col, tags] of Object.entries(table.columns ?? {})) {
|
|
323
|
+
if (tags?.pii || tags?.sensitive) loadClassify().addPiiFieldName(names, col);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return names;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function maskPiiWalk(value: unknown, fields: ReadonlySet<string>): unknown {
|
|
331
|
+
if (fields.size === 0 || value === null || value === undefined) return value;
|
|
332
|
+
if (Array.isArray(value)) return value.map((item) => maskPiiWalk(item, fields));
|
|
333
|
+
if (typeof value === "object") {
|
|
334
|
+
const out: Record<string, unknown> = {};
|
|
335
|
+
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
|
|
336
|
+
out[key] = fields.has(key) ? loadClassify().PII_MASK : maskPiiWalk(child, fields);
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
return value;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function aiRunsList(
|
|
344
|
+
ctx: McpContext,
|
|
345
|
+
requester: McpRequester,
|
|
346
|
+
args: Record<string, unknown>,
|
|
347
|
+
): Promise<ToolCallResult> {
|
|
348
|
+
const limit =
|
|
349
|
+
typeof args.limit === "number" && Number.isFinite(args.limit)
|
|
350
|
+
? Math.min(200, Math.max(1, Math.floor(args.limit)))
|
|
351
|
+
: 50;
|
|
352
|
+
const scope = visibleTenant(requester, args);
|
|
353
|
+
if (!scope.ok) return okData(freezeData({ runs: [] }), "trace");
|
|
354
|
+
const all = (await ctx.listAgentRuns?.()) ?? [];
|
|
355
|
+
const runs = all
|
|
356
|
+
.filter((row) => sameTenant(row, scope.tenant))
|
|
357
|
+
.slice(0, limit)
|
|
358
|
+
.map((row) => maskAiValue(ctx.getManifest(), row));
|
|
359
|
+
return okData(freezeData({ runs }), "trace");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function aiRunsGet(
|
|
363
|
+
ctx: McpContext,
|
|
364
|
+
requester: McpRequester,
|
|
365
|
+
args: Record<string, unknown>,
|
|
366
|
+
): Promise<ToolCallResult> {
|
|
367
|
+
const runId = String(args.runId ?? "");
|
|
368
|
+
const scope = visibleTenant(requester, args);
|
|
369
|
+
const found = scope.ok ? await ctx.getAgentRun?.(runId) : undefined;
|
|
370
|
+
if (!found || !sameTenant(found.run, scope.ok ? scope.tenant : undefined)) {
|
|
371
|
+
return {
|
|
372
|
+
ok: false,
|
|
373
|
+
code: "not-found",
|
|
374
|
+
message: `unknown run: ${runId}`,
|
|
375
|
+
data: asData({ runId }, "error"),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return okData(
|
|
379
|
+
freezeData({
|
|
380
|
+
run: maskAiValue(ctx.getManifest(), found.run),
|
|
381
|
+
events: maskAiValue(ctx.getManifest(), found.events),
|
|
382
|
+
}),
|
|
383
|
+
"store-record",
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function aiApprovalsList(
|
|
388
|
+
ctx: McpContext,
|
|
389
|
+
requester: McpRequester,
|
|
390
|
+
args: Record<string, unknown>,
|
|
391
|
+
): Promise<ToolCallResult> {
|
|
392
|
+
const scope = visibleTenant(requester, args);
|
|
393
|
+
if (!scope.ok) return okData(freezeData({ rows: [] }), "store-record");
|
|
394
|
+
const all = (await ctx.listApprovals?.()) ?? [];
|
|
395
|
+
const rows = all
|
|
396
|
+
.filter((row) => sameTenant(row, scope.tenant))
|
|
397
|
+
.map((row) => maskAiValue(ctx.getManifest(), row));
|
|
398
|
+
return okData(freezeData({ rows }), "store-record");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function decisionsList(
|
|
402
|
+
ctx: McpContext,
|
|
403
|
+
requester: McpRequester,
|
|
404
|
+
args: Record<string, unknown>,
|
|
405
|
+
): Promise<ToolCallResult> {
|
|
406
|
+
const scope = visibleTenant(requester, args);
|
|
407
|
+
if (!scope.ok) return okData(freezeData({ decisions: [] }), "manifest");
|
|
408
|
+
const decisions = ((await ctx.listDecisions?.(scope.tenant)) ?? []).filter((row) =>
|
|
409
|
+
sameTenant(row, scope.tenant),
|
|
410
|
+
);
|
|
411
|
+
return okData(freezeData({ decisions: maskAiValue(ctx.getManifest(), decisions) }), "manifest");
|
|
412
|
+
}
|
|
413
|
+
|
|
246
414
|
function actionConfirm(
|
|
247
415
|
confirm: ReturnType<typeof createConfirmationGate>,
|
|
248
416
|
requester: McpRequester,
|
|
@@ -18,7 +18,7 @@ import { otp } from "./otp.ts";
|
|
|
18
18
|
import { magicLink } from "./magic-link.ts";
|
|
19
19
|
|
|
20
20
|
const SECRET = "test-secret-at-least-16";
|
|
21
|
-
const MAILPIT_IMAGE = "axllent/mailpit:v1.31.
|
|
21
|
+
const MAILPIT_IMAGE = "axllent/mailpit:v1.31.2";
|
|
22
22
|
|
|
23
23
|
afterEach(() => {
|
|
24
24
|
resetBindings();
|
package/src/plugins/otp.test.ts
CHANGED
|
@@ -117,7 +117,71 @@ describe("otp() provider mode boot", () => {
|
|
|
117
117
|
const body = (await res.json()) as { data: { ok: true; devOtp?: string } };
|
|
118
118
|
expect(body.data.ok).toBe(true);
|
|
119
119
|
expect(body.data.devOtp).toBeUndefined();
|
|
120
|
-
expect(app.router.match("POST", "/auth/otp/resend")).
|
|
120
|
+
expect(app.router.match("POST", "/auth/otp/resend")).toBeTruthy();
|
|
121
|
+
await app.stop();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("SMS resend issues a new request id, honors cooldown, and rejects another channel", async () => {
|
|
125
|
+
let now = 1_000_000;
|
|
126
|
+
const requestIds: string[] = [];
|
|
127
|
+
const verifications = createVerificationStore();
|
|
128
|
+
const driver = mockOtpSmsDriver();
|
|
129
|
+
const transport = driver.smsTransport as unknown as SmsOtpTransport;
|
|
130
|
+
const original = transport.sendOtp.bind(transport);
|
|
131
|
+
transport.sendOtp = async (opts) => {
|
|
132
|
+
requestIds.push(opts.requestId);
|
|
133
|
+
return original(opts);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const app = oke({
|
|
137
|
+
name: `otp-provider-resend-${crypto.randomUUID()}`,
|
|
138
|
+
env: "test",
|
|
139
|
+
registry: "ignore",
|
|
140
|
+
gate: { auth: { secret: SECRET } },
|
|
141
|
+
channel: { drivers: [driver] },
|
|
142
|
+
fx: { now: () => now },
|
|
143
|
+
}).plug(
|
|
144
|
+
otp({
|
|
145
|
+
mode: "provider",
|
|
146
|
+
resendCooldownMs: 60_000,
|
|
147
|
+
verifications,
|
|
148
|
+
now: () => now,
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
await app.boot({ env: "test" });
|
|
152
|
+
|
|
153
|
+
const phone = "+15551234567";
|
|
154
|
+
expect((await app.fetch(jsonPost("/auth/otp/request", { phone }))).status).toBe(200);
|
|
155
|
+
const first = findActiveVerification(verifications, `otp:${phone}`, now);
|
|
156
|
+
expect(first?.value.startsWith("provider:")).toBe(true);
|
|
157
|
+
expect(first?.sealedOtp).toBeNull();
|
|
158
|
+
const firstId = first?.value.slice("provider:".length);
|
|
159
|
+
|
|
160
|
+
const cooled = await app.fetch(jsonPost("/auth/otp/resend", { phone }));
|
|
161
|
+
const cooledBody = (await cooled.json()) as { error?: { data?: { reason?: string } } };
|
|
162
|
+
expect(cooledBody.error?.data?.reason).toBe("resend_cooldown");
|
|
163
|
+
|
|
164
|
+
const other = await app.fetch(jsonPost("/auth/otp/resend", { phone, channel: "email" }));
|
|
165
|
+
const otherBody = (await other.json()) as { error?: { data?: { reason?: string } } };
|
|
166
|
+
expect(otherBody.error?.data?.reason).toBe("invalid_channel");
|
|
167
|
+
|
|
168
|
+
now += 60_001;
|
|
169
|
+
const resent = await app.fetch(jsonPost("/auth/otp/resend", { phone, channel: "sms" }));
|
|
170
|
+
expect(resent.status).toBe(200);
|
|
171
|
+
const resentBody = (await resent.json()) as { data: { channel: string; devOtp?: string } };
|
|
172
|
+
expect(resentBody.data.channel).toBe("sms");
|
|
173
|
+
expect(resentBody.data.devOtp).toBeUndefined();
|
|
174
|
+
|
|
175
|
+
const second = findActiveVerification(verifications, `otp:${phone}`, now);
|
|
176
|
+
const secondId = second?.value.slice("provider:".length);
|
|
177
|
+
expect(secondId).toBeDefined();
|
|
178
|
+
expect(secondId).not.toBe(firstId);
|
|
179
|
+
if (firstId === undefined || secondId === undefined) {
|
|
180
|
+
throw new Error("expected provider request ids");
|
|
181
|
+
}
|
|
182
|
+
expect(requestIds).toEqual([firstId, secondId]);
|
|
183
|
+
expect(findActiveVerification(verifications, `otp:${phone}`, now)?.sealedOtp).toBeNull();
|
|
184
|
+
|
|
121
185
|
await app.stop();
|
|
122
186
|
});
|
|
123
187
|
});
|