@kindgi/api 0.0.0-bootstrap.0 → 0.1.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/LICENSE +201 -0
- package/README.md +216 -1
- package/dist/adapter-binding.d.ts +177 -0
- package/dist/adapter-binding.d.ts.map +1 -0
- package/dist/adapter-binding.js +12 -0
- package/dist/adapter-binding.js.map +1 -0
- package/dist/agent-binding.d.ts +207 -0
- package/dist/agent-binding.d.ts.map +1 -0
- package/dist/agent-binding.js +4 -0
- package/dist/agent-binding.js.map +1 -0
- package/dist/app.d.ts +770 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +396 -0
- package/dist/app.js.map +1 -0
- package/dist/capability-binding.d.ts +76 -0
- package/dist/capability-binding.d.ts.map +1 -0
- package/dist/capability-binding.js +4 -0
- package/dist/capability-binding.js.map +1 -0
- package/dist/cost-binding.d.ts +175 -0
- package/dist/cost-binding.d.ts.map +1 -0
- package/dist/cost-binding.js +13 -0
- package/dist/cost-binding.js.map +1 -0
- package/dist/deployment-binding.d.ts +201 -0
- package/dist/deployment-binding.d.ts.map +1 -0
- package/dist/deployment-binding.js +4 -0
- package/dist/deployment-binding.js.map +1 -0
- package/dist/env-binding.d.ts +132 -0
- package/dist/env-binding.d.ts.map +1 -0
- package/dist/env-binding.js +4 -0
- package/dist/env-binding.js.map +1 -0
- package/dist/errors.d.ts +37 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +264 -0
- package/dist/errors.js.map +1 -0
- package/dist/eval-run-binding.d.ts +196 -0
- package/dist/eval-run-binding.d.ts.map +1 -0
- package/dist/eval-run-binding.js +16 -0
- package/dist/eval-run-binding.js.map +1 -0
- package/dist/eval-run-dispatcher.d.ts +131 -0
- package/dist/eval-run-dispatcher.d.ts.map +1 -0
- package/dist/eval-run-dispatcher.js +368 -0
- package/dist/eval-run-dispatcher.js.map +1 -0
- package/dist/eval-suite-binding.d.ts +227 -0
- package/dist/eval-suite-binding.d.ts.map +1 -0
- package/dist/eval-suite-binding.js +24 -0
- package/dist/eval-suite-binding.js.map +1 -0
- package/dist/event-bus-binding.d.ts +107 -0
- package/dist/event-bus-binding.d.ts.map +1 -0
- package/dist/event-bus-binding.js +5 -0
- package/dist/event-bus-binding.js.map +1 -0
- package/dist/flow-binding.d.ts +194 -0
- package/dist/flow-binding.d.ts.map +1 -0
- package/dist/flow-binding.js +4 -0
- package/dist/flow-binding.js.map +1 -0
- package/dist/guardrail-binding.d.ts +138 -0
- package/dist/guardrail-binding.d.ts.map +1 -0
- package/dist/guardrail-binding.js +4 -0
- package/dist/guardrail-binding.js.map +1 -0
- package/dist/handler-binding.d.ts +126 -0
- package/dist/handler-binding.d.ts.map +1 -0
- package/dist/handler-binding.js +4 -0
- package/dist/handler-binding.js.map +1 -0
- package/dist/hitl-binding.d.ts +115 -0
- package/dist/hitl-binding.d.ts.map +1 -0
- package/dist/hitl-binding.js +4 -0
- package/dist/hitl-binding.js.map +1 -0
- package/dist/identity-directory-binding.d.ts +116 -0
- package/dist/identity-directory-binding.d.ts.map +1 -0
- package/dist/identity-directory-binding.js +4 -0
- package/dist/identity-directory-binding.js.map +1 -0
- package/dist/identity-provider-binding.d.ts +184 -0
- package/dist/identity-provider-binding.d.ts.map +1 -0
- package/dist/identity-provider-binding.js +4 -0
- package/dist/identity-provider-binding.js.map +1 -0
- package/dist/image-registry-binding.d.ts +165 -0
- package/dist/image-registry-binding.d.ts.map +1 -0
- package/dist/image-registry-binding.js +4 -0
- package/dist/image-registry-binding.js.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/key-management-binding.d.ts +101 -0
- package/dist/key-management-binding.d.ts.map +1 -0
- package/dist/key-management-binding.js +4 -0
- package/dist/key-management-binding.js.map +1 -0
- package/dist/mcp-endpoint-binding.d.ts +312 -0
- package/dist/mcp-endpoint-binding.d.ts.map +1 -0
- package/dist/mcp-endpoint-binding.js +12 -0
- package/dist/mcp-endpoint-binding.js.map +1 -0
- package/dist/memory-binding.d.ts +209 -0
- package/dist/memory-binding.d.ts.map +1 -0
- package/dist/memory-binding.js +4 -0
- package/dist/memory-binding.js.map +1 -0
- package/dist/middleware/auth.d.ts +168 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +247 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/authorize.d.ts +26 -0
- package/dist/middleware/authorize.d.ts.map +1 -0
- package/dist/middleware/authorize.js +65 -0
- package/dist/middleware/authorize.js.map +1 -0
- package/dist/middleware/error-mapper.d.ts +13 -0
- package/dist/middleware/error-mapper.d.ts.map +1 -0
- package/dist/middleware/error-mapper.js +32 -0
- package/dist/middleware/error-mapper.js.map +1 -0
- package/dist/middleware/idempotency.d.ts +43 -0
- package/dist/middleware/idempotency.d.ts.map +1 -0
- package/dist/middleware/idempotency.js +118 -0
- package/dist/middleware/idempotency.js.map +1 -0
- package/dist/middleware/principal.d.ts +4 -0
- package/dist/middleware/principal.d.ts.map +1 -0
- package/dist/middleware/principal.js +42 -0
- package/dist/middleware/principal.js.map +1 -0
- package/dist/middleware/public-run-routes.d.ts +16 -0
- package/dist/middleware/public-run-routes.d.ts.map +1 -0
- package/dist/middleware/public-run-routes.js +43 -0
- package/dist/middleware/public-run-routes.js.map +1 -0
- package/dist/middleware/request-id.d.ts +14 -0
- package/dist/middleware/request-id.d.ts.map +1 -0
- package/dist/middleware/request-id.js +24 -0
- package/dist/middleware/request-id.js.map +1 -0
- package/dist/middleware/sigv4-verify.d.ts +64 -0
- package/dist/middleware/sigv4-verify.d.ts.map +1 -0
- package/dist/middleware/sigv4-verify.js +286 -0
- package/dist/middleware/sigv4-verify.js.map +1 -0
- package/dist/middleware/sigv4.d.ts +25 -0
- package/dist/middleware/sigv4.d.ts.map +1 -0
- package/dist/middleware/sigv4.js +120 -0
- package/dist/middleware/sigv4.js.map +1 -0
- package/dist/openapi/generate.d.ts +15 -0
- package/dist/openapi/generate.d.ts.map +1 -0
- package/dist/openapi/generate.js +241 -0
- package/dist/openapi/generate.js.map +1 -0
- package/dist/openapi/operations.d.ts +70 -0
- package/dist/openapi/operations.d.ts.map +1 -0
- package/dist/openapi/operations.js +5051 -0
- package/dist/openapi/operations.js.map +1 -0
- package/dist/openapi/schemas.d.ts +590 -0
- package/dist/openapi/schemas.d.ts.map +1 -0
- package/dist/openapi/schemas.js +5820 -0
- package/dist/openapi/schemas.js.map +1 -0
- package/dist/provenance-binding.d.ts +116 -0
- package/dist/provenance-binding.d.ts.map +1 -0
- package/dist/provenance-binding.js +31 -0
- package/dist/provenance-binding.js.map +1 -0
- package/dist/provider-binding.d.ts +174 -0
- package/dist/provider-binding.d.ts.map +1 -0
- package/dist/provider-binding.js +4 -0
- package/dist/provider-binding.js.map +1 -0
- package/dist/public-run-token.d.ts +97 -0
- package/dist/public-run-token.d.ts.map +1 -0
- package/dist/public-run-token.js +167 -0
- package/dist/public-run-token.js.map +1 -0
- package/dist/retention-binding.d.ts +69 -0
- package/dist/retention-binding.d.ts.map +1 -0
- package/dist/retention-binding.js +4 -0
- package/dist/retention-binding.js.map +1 -0
- package/dist/reviewer-binding.d.ts +84 -0
- package/dist/reviewer-binding.d.ts.map +1 -0
- package/dist/reviewer-binding.js +4 -0
- package/dist/reviewer-binding.js.map +1 -0
- package/dist/rotation-status-store.d.ts +96 -0
- package/dist/rotation-status-store.d.ts.map +1 -0
- package/dist/rotation-status-store.js +106 -0
- package/dist/rotation-status-store.js.map +1 -0
- package/dist/routes/adapters.d.ts +29 -0
- package/dist/routes/adapters.d.ts.map +1 -0
- package/dist/routes/adapters.js +230 -0
- package/dist/routes/adapters.js.map +1 -0
- package/dist/routes/agents.d.ts +17 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +342 -0
- package/dist/routes/agents.js.map +1 -0
- package/dist/routes/approvals.d.ts +37 -0
- package/dist/routes/approvals.d.ts.map +1 -0
- package/dist/routes/approvals.js +551 -0
- package/dist/routes/approvals.js.map +1 -0
- package/dist/routes/artifacts.d.ts +25 -0
- package/dist/routes/artifacts.d.ts.map +1 -0
- package/dist/routes/artifacts.js +265 -0
- package/dist/routes/artifacts.js.map +1 -0
- package/dist/routes/audit.d.ts +6 -0
- package/dist/routes/audit.d.ts.map +1 -0
- package/dist/routes/audit.js +162 -0
- package/dist/routes/audit.js.map +1 -0
- package/dist/routes/auth.d.ts +47 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +630 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/capabilities.d.ts +14 -0
- package/dist/routes/capabilities.d.ts.map +1 -0
- package/dist/routes/capabilities.js +62 -0
- package/dist/routes/capabilities.js.map +1 -0
- package/dist/routes/compliance.d.ts +37 -0
- package/dist/routes/compliance.d.ts.map +1 -0
- package/dist/routes/compliance.js +321 -0
- package/dist/routes/compliance.js.map +1 -0
- package/dist/routes/conversations.d.ts +13 -0
- package/dist/routes/conversations.d.ts.map +1 -0
- package/dist/routes/conversations.js +295 -0
- package/dist/routes/conversations.js.map +1 -0
- package/dist/routes/cost.d.ts +24 -0
- package/dist/routes/cost.d.ts.map +1 -0
- package/dist/routes/cost.js +243 -0
- package/dist/routes/cost.js.map +1 -0
- package/dist/routes/deployments.d.ts +122 -0
- package/dist/routes/deployments.d.ts.map +1 -0
- package/dist/routes/deployments.js +1036 -0
- package/dist/routes/deployments.js.map +1 -0
- package/dist/routes/env.d.ts +62 -0
- package/dist/routes/env.d.ts.map +1 -0
- package/dist/routes/env.js +311 -0
- package/dist/routes/env.js.map +1 -0
- package/dist/routes/eval-runs.d.ts +28 -0
- package/dist/routes/eval-runs.d.ts.map +1 -0
- package/dist/routes/eval-runs.js +479 -0
- package/dist/routes/eval-runs.js.map +1 -0
- package/dist/routes/eval-suites.d.ts +24 -0
- package/dist/routes/eval-suites.d.ts.map +1 -0
- package/dist/routes/eval-suites.js +381 -0
- package/dist/routes/eval-suites.js.map +1 -0
- package/dist/routes/event-triggers.d.ts +12 -0
- package/dist/routes/event-triggers.d.ts.map +1 -0
- package/dist/routes/event-triggers.js +218 -0
- package/dist/routes/event-triggers.js.map +1 -0
- package/dist/routes/flows.d.ts +23 -0
- package/dist/routes/flows.d.ts.map +1 -0
- package/dist/routes/flows.js +344 -0
- package/dist/routes/flows.js.map +1 -0
- package/dist/routes/guardrails.d.ts +33 -0
- package/dist/routes/guardrails.d.ts.map +1 -0
- package/dist/routes/guardrails.js +236 -0
- package/dist/routes/guardrails.js.map +1 -0
- package/dist/routes/identity.d.ts +39 -0
- package/dist/routes/identity.d.ts.map +1 -0
- package/dist/routes/identity.js +138 -0
- package/dist/routes/identity.js.map +1 -0
- package/dist/routes/mcp.d.ts +23 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +727 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/memory.d.ts +16 -0
- package/dist/routes/memory.d.ts.map +1 -0
- package/dist/routes/memory.js +353 -0
- package/dist/routes/memory.js.map +1 -0
- package/dist/routes/observations.d.ts +19 -0
- package/dist/routes/observations.d.ts.map +1 -0
- package/dist/routes/observations.js +135 -0
- package/dist/routes/observations.js.map +1 -0
- package/dist/routes/orgs.d.ts +24 -0
- package/dist/routes/orgs.d.ts.map +1 -0
- package/dist/routes/orgs.js +208 -0
- package/dist/routes/orgs.js.map +1 -0
- package/dist/routes/pagination.d.ts +18 -0
- package/dist/routes/pagination.d.ts.map +1 -0
- package/dist/routes/pagination.js +35 -0
- package/dist/routes/pagination.js.map +1 -0
- package/dist/routes/policies.d.ts +23 -0
- package/dist/routes/policies.d.ts.map +1 -0
- package/dist/routes/policies.js +314 -0
- package/dist/routes/policies.js.map +1 -0
- package/dist/routes/projects.d.ts +45 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +485 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/proposals.d.ts +24 -0
- package/dist/routes/proposals.d.ts.map +1 -0
- package/dist/routes/proposals.js +581 -0
- package/dist/routes/proposals.js.map +1 -0
- package/dist/routes/provenance.d.ts +34 -0
- package/dist/routes/provenance.d.ts.map +1 -0
- package/dist/routes/provenance.js +274 -0
- package/dist/routes/provenance.js.map +1 -0
- package/dist/routes/providers.d.ts +32 -0
- package/dist/routes/providers.d.ts.map +1 -0
- package/dist/routes/providers.js +490 -0
- package/dist/routes/providers.js.map +1 -0
- package/dist/routes/public-run-tokens.d.ts +17 -0
- package/dist/routes/public-run-tokens.d.ts.map +1 -0
- package/dist/routes/public-run-tokens.js +69 -0
- package/dist/routes/public-run-tokens.js.map +1 -0
- package/dist/routes/retention.d.ts +19 -0
- package/dist/routes/retention.d.ts.map +1 -0
- package/dist/routes/retention.js +152 -0
- package/dist/routes/retention.js.map +1 -0
- package/dist/routes/reviewers.d.ts +5 -0
- package/dist/routes/reviewers.d.ts.map +1 -0
- package/dist/routes/reviewers.js +161 -0
- package/dist/routes/reviewers.js.map +1 -0
- package/dist/routes/runs.d.ts +34 -0
- package/dist/routes/runs.d.ts.map +1 -0
- package/dist/routes/runs.js +773 -0
- package/dist/routes/runs.js.map +1 -0
- package/dist/routes/s3.d.ts +31 -0
- package/dist/routes/s3.d.ts.map +1 -0
- package/dist/routes/s3.js +433 -0
- package/dist/routes/s3.js.map +1 -0
- package/dist/routes/schedules.d.ts +20 -0
- package/dist/routes/schedules.d.ts.map +1 -0
- package/dist/routes/schedules.js +234 -0
- package/dist/routes/schedules.js.map +1 -0
- package/dist/routes/scope-params.d.ts +57 -0
- package/dist/routes/scope-params.d.ts.map +1 -0
- package/dist/routes/scope-params.js +105 -0
- package/dist/routes/scope-params.js.map +1 -0
- package/dist/routes/secret-ref.d.ts +20 -0
- package/dist/routes/secret-ref.d.ts.map +1 -0
- package/dist/routes/secret-ref.js +42 -0
- package/dist/routes/secret-ref.js.map +1 -0
- package/dist/routes/secrets.d.ts +53 -0
- package/dist/routes/secrets.d.ts.map +1 -0
- package/dist/routes/secrets.js +715 -0
- package/dist/routes/secrets.js.map +1 -0
- package/dist/routes/signing-keys.d.ts +23 -0
- package/dist/routes/signing-keys.d.ts.map +1 -0
- package/dist/routes/signing-keys.js +212 -0
- package/dist/routes/signing-keys.js.map +1 -0
- package/dist/routes/sse.d.ts +58 -0
- package/dist/routes/sse.d.ts.map +1 -0
- package/dist/routes/sse.js +92 -0
- package/dist/routes/sse.js.map +1 -0
- package/dist/routes/teams.d.ts +30 -0
- package/dist/routes/teams.d.ts.map +1 -0
- package/dist/routes/teams.js +443 -0
- package/dist/routes/teams.js.map +1 -0
- package/dist/routes/tenant.d.ts +57 -0
- package/dist/routes/tenant.d.ts.map +1 -0
- package/dist/routes/tenant.js +352 -0
- package/dist/routes/tenant.js.map +1 -0
- package/dist/routes/tokens.d.ts +14 -0
- package/dist/routes/tokens.d.ts.map +1 -0
- package/dist/routes/tokens.js +231 -0
- package/dist/routes/tokens.js.map +1 -0
- package/dist/routes/tools.d.ts +33 -0
- package/dist/routes/tools.d.ts.map +1 -0
- package/dist/routes/tools.js +380 -0
- package/dist/routes/tools.js.map +1 -0
- package/dist/routes/webhook-endpoints.d.ts +10 -0
- package/dist/routes/webhook-endpoints.d.ts.map +1 -0
- package/dist/routes/webhook-endpoints.js +407 -0
- package/dist/routes/webhook-endpoints.js.map +1 -0
- package/dist/routes/webhooks.d.ts +21 -0
- package/dist/routes/webhooks.d.ts.map +1 -0
- package/dist/routes/webhooks.js +228 -0
- package/dist/routes/webhooks.js.map +1 -0
- package/dist/s3-credential-binding.d.ts +54 -0
- package/dist/s3-credential-binding.d.ts.map +1 -0
- package/dist/s3-credential-binding.js +4 -0
- package/dist/s3-credential-binding.js.map +1 -0
- package/dist/secrets-binding.d.ts +281 -0
- package/dist/secrets-binding.d.ts.map +1 -0
- package/dist/secrets-binding.js +10 -0
- package/dist/secrets-binding.js.map +1 -0
- package/dist/secrets-provider-binding.d.ts +164 -0
- package/dist/secrets-provider-binding.d.ts.map +1 -0
- package/dist/secrets-provider-binding.js +4 -0
- package/dist/secrets-provider-binding.js.map +1 -0
- package/dist/session-store-binding.d.ts +148 -0
- package/dist/session-store-binding.d.ts.map +1 -0
- package/dist/session-store-binding.js +4 -0
- package/dist/session-store-binding.js.map +1 -0
- package/dist/signing-key-binding.d.ts +210 -0
- package/dist/signing-key-binding.d.ts.map +1 -0
- package/dist/signing-key-binding.js +4 -0
- package/dist/signing-key-binding.js.map +1 -0
- package/dist/state-store-binding.d.ts +53 -0
- package/dist/state-store-binding.d.ts.map +1 -0
- package/dist/state-store-binding.js +38 -0
- package/dist/state-store-binding.js.map +1 -0
- package/dist/supervisor-binding.d.ts +526 -0
- package/dist/supervisor-binding.d.ts.map +1 -0
- package/dist/supervisor-binding.js +4 -0
- package/dist/supervisor-binding.js.map +1 -0
- package/dist/tenant-host-access.d.ts +41 -0
- package/dist/tenant-host-access.d.ts.map +1 -0
- package/dist/tenant-host-access.js +42 -0
- package/dist/tenant-host-access.js.map +1 -0
- package/dist/token-admin.d.ts +99 -0
- package/dist/token-admin.d.ts.map +1 -0
- package/dist/token-admin.js +4 -0
- package/dist/token-admin.js.map +1 -0
- package/dist/tool-binding.d.ts +247 -0
- package/dist/tool-binding.d.ts.map +1 -0
- package/dist/tool-binding.js +4 -0
- package/dist/tool-binding.js.map +1 -0
- package/dist/trigger-binding.d.ts +13 -0
- package/dist/trigger-binding.d.ts.map +1 -0
- package/dist/trigger-binding.js +4 -0
- package/dist/trigger-binding.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/webhook-endpoint-binding.d.ts +230 -0
- package/dist/webhook-endpoint-binding.d.ts.map +1 -0
- package/dist/webhook-endpoint-binding.js +12 -0
- package/dist/webhook-endpoint-binding.js.map +1 -0
- package/dist/xml.d.ts +79 -0
- package/dist/xml.d.ts.map +1 -0
- package/dist/xml.js +115 -0
- package/dist/xml.js.map +1 -0
- package/openapi.json +29659 -0
- package/package.json +76 -4
- package/scripts/emit-openapi.mjs +44 -0
- package/src/adapter-binding.ts +204 -0
- package/src/agent-binding.ts +231 -0
- package/src/app.ts +1285 -0
- package/src/capability-binding.ts +83 -0
- package/src/cost-binding.ts +213 -0
- package/src/deployment-binding.ts +225 -0
- package/src/env-binding.ts +140 -0
- package/src/errors.ts +291 -0
- package/src/eval-run-binding.ts +209 -0
- package/src/eval-run-dispatcher.ts +497 -0
- package/src/eval-suite-binding.ts +257 -0
- package/src/event-bus-binding.ts +127 -0
- package/src/flow-binding.ts +213 -0
- package/src/guardrail-binding.ts +151 -0
- package/src/handler-binding.ts +132 -0
- package/src/hitl-binding.ts +172 -0
- package/src/identity-directory-binding.ts +128 -0
- package/src/identity-provider-binding.ts +203 -0
- package/src/image-registry-binding.ts +193 -0
- package/src/index.ts +606 -0
- package/src/key-management-binding.ts +91 -0
- package/src/mcp-endpoint-binding.ts +311 -0
- package/src/memory-binding.ts +239 -0
- package/src/middleware/auth.ts +437 -0
- package/src/middleware/authorize.ts +125 -0
- package/src/middleware/error-mapper.ts +37 -0
- package/src/middleware/idempotency.ts +160 -0
- package/src/middleware/principal.ts +64 -0
- package/src/middleware/public-run-routes.ts +50 -0
- package/src/middleware/request-id.ts +28 -0
- package/src/middleware/sigv4-verify.ts +374 -0
- package/src/middleware/sigv4.ts +165 -0
- package/src/openapi/generate.ts +307 -0
- package/src/openapi/operations.ts +5445 -0
- package/src/openapi/schemas.ts +6363 -0
- package/src/provenance-binding.ts +169 -0
- package/src/provider-binding.ts +196 -0
- package/src/public-run-token.ts +263 -0
- package/src/retention-binding.ts +78 -0
- package/src/reviewer-binding.ts +99 -0
- package/src/rotation-status-store.ts +209 -0
- package/src/routes/adapters.ts +319 -0
- package/src/routes/agents.ts +424 -0
- package/src/routes/approvals.ts +759 -0
- package/src/routes/artifacts.ts +317 -0
- package/src/routes/audit.ts +219 -0
- package/src/routes/auth.ts +806 -0
- package/src/routes/capabilities.ts +79 -0
- package/src/routes/compliance.ts +482 -0
- package/src/routes/conversations.ts +369 -0
- package/src/routes/cost.ts +327 -0
- package/src/routes/deployments.ts +1493 -0
- package/src/routes/env.ts +429 -0
- package/src/routes/eval-runs.ts +631 -0
- package/src/routes/eval-suites.ts +490 -0
- package/src/routes/event-triggers.ts +283 -0
- package/src/routes/flows.ts +423 -0
- package/src/routes/guardrails.ts +320 -0
- package/src/routes/identity.ts +203 -0
- package/src/routes/mcp.ts +940 -0
- package/src/routes/memory.ts +430 -0
- package/src/routes/observations.ts +175 -0
- package/src/routes/orgs.ts +273 -0
- package/src/routes/pagination.ts +51 -0
- package/src/routes/policies.ts +413 -0
- package/src/routes/projects.ts +634 -0
- package/src/routes/proposals.ts +719 -0
- package/src/routes/provenance.ts +406 -0
- package/src/routes/providers.ts +624 -0
- package/src/routes/public-run-tokens.ts +104 -0
- package/src/routes/retention.ts +193 -0
- package/src/routes/reviewers.ts +209 -0
- package/src/routes/runs.ts +981 -0
- package/src/routes/s3.ts +519 -0
- package/src/routes/schedules.ts +300 -0
- package/src/routes/scope-params.ts +133 -0
- package/src/routes/secret-ref.ts +52 -0
- package/src/routes/secrets.ts +975 -0
- package/src/routes/signing-keys.ts +259 -0
- package/src/routes/sse.ts +148 -0
- package/src/routes/teams.ts +584 -0
- package/src/routes/tenant.ts +530 -0
- package/src/routes/tokens.ts +285 -0
- package/src/routes/tools.ts +491 -0
- package/src/routes/webhook-endpoints.ts +534 -0
- package/src/routes/webhooks.ts +296 -0
- package/src/s3-credential-binding.ts +58 -0
- package/src/secrets-binding.ts +308 -0
- package/src/secrets-provider-binding.ts +167 -0
- package/src/session-store-binding.ts +163 -0
- package/src/signing-key-binding.ts +235 -0
- package/src/state-store-binding.ts +84 -0
- package/src/supervisor-binding.ts +554 -0
- package/src/tenant-host-access.ts +62 -0
- package/src/token-admin.ts +104 -0
- package/src/tool-binding.ts +274 -0
- package/src/trigger-binding.ts +38 -0
- package/src/types.ts +74 -0
- package/src/webhook-endpoint-binding.ts +246 -0
- package/src/xml.ts +196 -0
|
@@ -0,0 +1,1493 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
import { Hono } from 'hono';
|
|
7
|
+
|
|
8
|
+
import type { AgentId } from '@kindgi/agents';
|
|
9
|
+
import { tuplesForCreate } from '@kindgi/authz';
|
|
10
|
+
import { parsePublicKeyPem, verifyEd25519 } from '@kindgi/crypto';
|
|
11
|
+
import { type Guardrail, validateGuardrailSpec } from '@kindgi/guardrails';
|
|
12
|
+
import type { ProjectBinding, Scope } from '@kindgi/platform';
|
|
13
|
+
import { validateToolManifest } from '@kindgi/tools';
|
|
14
|
+
import type {
|
|
15
|
+
Cursor,
|
|
16
|
+
FlowId,
|
|
17
|
+
GuardrailId,
|
|
18
|
+
ProjectId,
|
|
19
|
+
SigningKeyId,
|
|
20
|
+
TenantId,
|
|
21
|
+
ToolId,
|
|
22
|
+
} from '@kindgi/types';
|
|
23
|
+
|
|
24
|
+
import type { AgentRegistryBinding } from '../agent-binding.js';
|
|
25
|
+
import type {
|
|
26
|
+
Deployment,
|
|
27
|
+
DeploymentBinding,
|
|
28
|
+
DeploymentPrimitiveCounts,
|
|
29
|
+
DeploymentRegisterOutcome,
|
|
30
|
+
} from '../deployment-binding.js';
|
|
31
|
+
import { statusFor, toWireError } from '../errors.js';
|
|
32
|
+
import type { FlowRegistryBinding } from '../flow-binding.js';
|
|
33
|
+
import type { GuardrailRegistryBinding } from '../guardrail-binding.js';
|
|
34
|
+
import type { ImageRegistryBinding } from '../image-registry-binding.js';
|
|
35
|
+
import type { SecretBinding } from '../secrets-binding.js';
|
|
36
|
+
import type { SigningKeyBinding as SigningKeyRegistryBinding } from '../signing-key-binding.js';
|
|
37
|
+
import type { ToolRegistryBinding } from '../tool-binding.js';
|
|
38
|
+
import type { AppEnv } from '../types.js';
|
|
39
|
+
import { hasCapability, requireEnvName } from './env.js';
|
|
40
|
+
import type { GuardrailWriteHook } from './guardrails.js';
|
|
41
|
+
import { clampLimit } from './pagination.js';
|
|
42
|
+
import type { ToolWriteHook } from './tools.js';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Deployments resource routes — the landing surface for signed pack
|
|
46
|
+
* deploys:
|
|
47
|
+
*
|
|
48
|
+
* - `POST /v1/deployments` — signed deployment (atomic).
|
|
49
|
+
* - `GET /v1/deployments` — cursor-paginated list.
|
|
50
|
+
* - `GET /v1/deployments/:deploymentId` — single record.
|
|
51
|
+
* - `POST /v1/deployments/:deploymentId/secrets` — deployment-scoped secret sync.
|
|
52
|
+
*
|
|
53
|
+
* ### Atomic transaction (POST)
|
|
54
|
+
*
|
|
55
|
+
* The route runs four verification steps IN ORDER (1–4 below) before any
|
|
56
|
+
* registry write, then does upserts + records deployment (5–6) as a single
|
|
57
|
+
* all-or-nothing unit. Any failure past the first upsert rolls back the successfully-
|
|
58
|
+
* inserted primitives so the tenant's catalog never observes a partial
|
|
59
|
+
* deploy.
|
|
60
|
+
*
|
|
61
|
+
* 1. Body shape (id/version/signature fields present + typed).
|
|
62
|
+
* 2. Signature — canonicalise `{imageDigest, artifactVersion,
|
|
63
|
+
* indexHash, tenantId, publishedAt}` as sorted-key JSON (UTF-8,
|
|
64
|
+
* no whitespace); verify with Ed25519 against `signerPublicKey`;
|
|
65
|
+
* cross-check `(signerKeyId, publicKey)` on the tenant's trust
|
|
66
|
+
* list via `SigningKeyRegistryBinding.isTrusted`.
|
|
67
|
+
* 3. Image — probe via `ImageRegistryBinding.head`, verify digest in
|
|
68
|
+
* the image ref matches the descriptor; extract `/app/index.json`,
|
|
69
|
+
* verify its sha256 matches the signed `indexHash` and its
|
|
70
|
+
* `artifactVersion` is the signed one.
|
|
71
|
+
* 4. Manifest validation — the image's `/app/index.json` (not a copy
|
|
72
|
+
* from the request: only the image's bytes are signed). Each
|
|
73
|
+
* `tools[]`, `guardrails[]`, `agents[]`, `flows[]` runs through its
|
|
74
|
+
* validator. A tool's `modulePath` and a
|
|
75
|
+
* guardrail's `checkModulePath` become `codeArtifactRef: { kind:
|
|
76
|
+
* 'oci', imageRef, modulePath, artifactVersion }`, which is how the
|
|
77
|
+
* runtime knows the code runs in the pack service; a guardrail's
|
|
78
|
+
* `checkId` becomes its `check`. Agents and flows are declarative:
|
|
79
|
+
* their `modulePath` is dropped.
|
|
80
|
+
* 5. Upserts — tools + guardrails + agents + flows. Track which were
|
|
81
|
+
* `ok` (newly registered) vs `already-registered` so rollback only
|
|
82
|
+
* unwinds our own writes.
|
|
83
|
+
* 6. Deployment record via `DeploymentBinding.register`. On failure,
|
|
84
|
+
* rollback all `ok` upserts and return 500.
|
|
85
|
+
*
|
|
86
|
+
* ### Idempotency
|
|
87
|
+
*
|
|
88
|
+
* Two layers:
|
|
89
|
+
* - `Idempotency-Key` middleware caches the full HTTP response — a
|
|
90
|
+
* retry with the same key + same body replays the original 201.
|
|
91
|
+
* - Digest idempotency handled inside `DeploymentBinding.register`:
|
|
92
|
+
* the same `imageDigest` re-submitted under the same tenant returns
|
|
93
|
+
* `{ kind: 'already-registered', deployment }` and the route surfaces
|
|
94
|
+
* it as `200` (NOT `201`), same `deploymentId` + `artifactVersion`.
|
|
95
|
+
*
|
|
96
|
+
* ### Immutable records
|
|
97
|
+
*
|
|
98
|
+
* No PATCH, no DELETE. Each record lists exactly what it shipped
|
|
99
|
+
* (`contents`: ids and versions). Versions are immutable and an image
|
|
100
|
+
* digest deploys once, so the latest deployment is the live one, and a
|
|
101
|
+
* rollback rolls forward: deploy the earlier code again, under new
|
|
102
|
+
* versions. (Re-activating an earlier record instead would need every
|
|
103
|
+
* resolver to resolve only within the active deployment's versions.)
|
|
104
|
+
*
|
|
105
|
+
* The route is register-only + read-only.
|
|
106
|
+
*/
|
|
107
|
+
export interface DeploymentsRouterBindings {
|
|
108
|
+
readonly deploymentRegistry: DeploymentBinding;
|
|
109
|
+
readonly signingKeyRegistry: SigningKeyRegistryBinding;
|
|
110
|
+
readonly imageRegistry: ImageRegistryBinding;
|
|
111
|
+
readonly toolRegistry?: ToolRegistryBinding;
|
|
112
|
+
readonly guardrailRegistry?: GuardrailRegistryBinding;
|
|
113
|
+
readonly agentRegistry?: AgentRegistryBinding;
|
|
114
|
+
readonly flowRegistry?: FlowRegistryBinding;
|
|
115
|
+
/**
|
|
116
|
+
* REQUIRED at runtime for `POST /v1/deployments/:deploymentId/secrets`.
|
|
117
|
+
* Optional at the type level so app compositions without a secrets
|
|
118
|
+
* binding keep booting — the sync route is always mounted and answers
|
|
119
|
+
* `500` when this is not wired; the register / list / get routes remain
|
|
120
|
+
* functional without it.
|
|
121
|
+
*/
|
|
122
|
+
readonly secretsBinding?: SecretBinding;
|
|
123
|
+
/**
|
|
124
|
+
* OPTIONAL at the type level, but REQUIRED at runtime any time
|
|
125
|
+
* `validated.agents.length > 0` OR `validated.flows.length > 0`
|
|
126
|
+
* OR `validated.tools.length > 0` OR `validated.guardrails.length > 0`.
|
|
127
|
+
* The agent / flow / tool / guardrail register loops resolve the
|
|
128
|
+
* tenant's Default project via `projectBinding.getDefault(tenantId)`
|
|
129
|
+
* and thread the resolved id through
|
|
130
|
+
* `AgentRegistryBinding.publish(...)` /
|
|
131
|
+
* `FlowRegistryBinding.publish(...)` /
|
|
132
|
+
* `ToolRegistryBinding.register(...)` /
|
|
133
|
+
* `GuardrailRegistryBinding.register(...)` (content-scoped writes
|
|
134
|
+
* always carry a project; deploys target the Default project).
|
|
135
|
+
* A deployment that carries any of those primitives without this
|
|
136
|
+
* binding wired fails fast at the loop entrypoint with a clear 500
|
|
137
|
+
* message. All four register loops resolve the project the same way.
|
|
138
|
+
*/
|
|
139
|
+
readonly projectBinding?: ProjectBinding;
|
|
140
|
+
/**
|
|
141
|
+
* Called for each tool and guardrail a new deployment registered, as
|
|
142
|
+
* `POST /v1/tools` and `POST /v1/guardrails` call them, so a runtime
|
|
143
|
+
* that caches the catalogs (its tool and guardrail bridges) sees the
|
|
144
|
+
* deployment at once rather than at restart. Advisory: a failing hook
|
|
145
|
+
* doesn't fail the deployment.
|
|
146
|
+
*/
|
|
147
|
+
readonly onToolWrite?: ToolWriteHook;
|
|
148
|
+
readonly onGuardrailWrite?: GuardrailWriteHook;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function deploymentsRouter(bindings: DeploymentsRouterBindings): Hono<AppEnv> {
|
|
152
|
+
const r = new Hono<AppEnv>();
|
|
153
|
+
|
|
154
|
+
// ---------- GET / (list, cursor-paginated) ----------
|
|
155
|
+
r.get('/', async (c) => {
|
|
156
|
+
const requestId = c.get('requestId');
|
|
157
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
158
|
+
const limit = clampLimit(c.req.query('limit'));
|
|
159
|
+
|
|
160
|
+
const cursorRaw = c.req.query('cursor');
|
|
161
|
+
const imageRefPrefixRaw = c.req.query('imageRefPrefix');
|
|
162
|
+
const signerKeyIdRaw = c.req.query('signerKeyId');
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const page = await bindings.deploymentRegistry.list({
|
|
166
|
+
tenantId,
|
|
167
|
+
limit,
|
|
168
|
+
...(cursorRaw !== undefined && cursorRaw.length > 0 && { cursor: cursorRaw as Cursor }),
|
|
169
|
+
...(imageRefPrefixRaw !== undefined &&
|
|
170
|
+
imageRefPrefixRaw.length > 0 && {
|
|
171
|
+
imageRefPrefix: imageRefPrefixRaw,
|
|
172
|
+
}),
|
|
173
|
+
...(signerKeyIdRaw !== undefined &&
|
|
174
|
+
signerKeyIdRaw.length > 0 && {
|
|
175
|
+
signerKeyId: signerKeyIdRaw as SigningKeyId,
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
return c.json({
|
|
179
|
+
data: page.data.map(serializeDeployment),
|
|
180
|
+
hasMore: page.nextCursor !== undefined,
|
|
181
|
+
...(page.nextCursor !== undefined && {
|
|
182
|
+
nextCursor: page.nextCursor as unknown as string,
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
} catch (cause) {
|
|
186
|
+
c.status(statusFor('internal-server-error') as never);
|
|
187
|
+
return c.json(
|
|
188
|
+
toWireError(
|
|
189
|
+
{
|
|
190
|
+
code: 'internal-server-error',
|
|
191
|
+
message: `Deployment list failed: ${errMessage(cause)}`,
|
|
192
|
+
},
|
|
193
|
+
requestId,
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ---------- GET /:deploymentId ----------
|
|
200
|
+
r.get('/:deploymentId', async (c) => {
|
|
201
|
+
const requestId = c.get('requestId');
|
|
202
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
203
|
+
const deploymentId = c.req.param('deploymentId');
|
|
204
|
+
|
|
205
|
+
const record = await bindings.deploymentRegistry.get({ tenantId, deploymentId });
|
|
206
|
+
if (record === null) {
|
|
207
|
+
c.status(statusFor('deployment-not-found') as never);
|
|
208
|
+
return c.json(
|
|
209
|
+
toWireError(
|
|
210
|
+
{
|
|
211
|
+
code: 'deployment-not-found',
|
|
212
|
+
message: `No deployment with id "${deploymentId}" under this tenant`,
|
|
213
|
+
deploymentId,
|
|
214
|
+
},
|
|
215
|
+
requestId,
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return c.json(serializeDeployment(record));
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ---------- POST / (register a signed deployment) ----------
|
|
223
|
+
r.post('/', async (c) => {
|
|
224
|
+
const requestId = c.get('requestId');
|
|
225
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
226
|
+
|
|
227
|
+
let body: unknown;
|
|
228
|
+
try {
|
|
229
|
+
body = await c.req.json();
|
|
230
|
+
} catch {
|
|
231
|
+
c.status(statusFor('bad-input') as never);
|
|
232
|
+
return c.json(
|
|
233
|
+
toWireError({ code: 'bad-input', message: 'Request body must be valid JSON' }, requestId),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const parsed = parseWireBody(body);
|
|
237
|
+
if (parsed.kind === 'err') {
|
|
238
|
+
c.status(statusFor('bad-input') as never);
|
|
239
|
+
return c.json(
|
|
240
|
+
toWireError(
|
|
241
|
+
{
|
|
242
|
+
code: 'bad-input',
|
|
243
|
+
message: parsed.error.message,
|
|
244
|
+
issues: parsed.error.issues as unknown as Record<string, unknown>[],
|
|
245
|
+
},
|
|
246
|
+
requestId,
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const wire = parsed.value;
|
|
251
|
+
|
|
252
|
+
// Extract image digest from imageRef. Must be `<host>/<repo>@sha256:<hex>`.
|
|
253
|
+
const digestFromRef = extractImageDigest(wire.imageRef);
|
|
254
|
+
if (digestFromRef === null) {
|
|
255
|
+
c.status(statusFor('bad-input') as never);
|
|
256
|
+
return c.json(
|
|
257
|
+
toWireError(
|
|
258
|
+
{
|
|
259
|
+
code: 'bad-input',
|
|
260
|
+
message: 'imageRef must be digest-pinned: "<host>/<repo>@sha256:<64-hex>"',
|
|
261
|
+
},
|
|
262
|
+
requestId,
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const imageDigest = digestFromRef;
|
|
267
|
+
|
|
268
|
+
// ---- Signature verification ----
|
|
269
|
+
const publicKeyBytes = parsePublicKeyPem(wire.signerPublicKey);
|
|
270
|
+
if (publicKeyBytes.kind === 'err') {
|
|
271
|
+
c.status(statusFor('bad-input') as never);
|
|
272
|
+
return c.json(
|
|
273
|
+
toWireError(
|
|
274
|
+
{
|
|
275
|
+
code: 'bad-input',
|
|
276
|
+
message: `signerPublicKey PEM invalid: ${publicKeyBytes.error.message}`,
|
|
277
|
+
},
|
|
278
|
+
requestId,
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
const publicKeyBase64 = Buffer.from(publicKeyBytes.value).toString('base64');
|
|
283
|
+
const trusted = await bindings.signingKeyRegistry.isTrusted({
|
|
284
|
+
tenantId,
|
|
285
|
+
keyId: wire.signerKeyId,
|
|
286
|
+
publicKey: publicKeyBase64,
|
|
287
|
+
});
|
|
288
|
+
if (trusted.kind === 'err') {
|
|
289
|
+
c.status(statusFor('internal-server-error') as never);
|
|
290
|
+
return c.json(
|
|
291
|
+
toWireError(
|
|
292
|
+
{
|
|
293
|
+
code: 'internal-server-error',
|
|
294
|
+
message: `Trust check failed: ${trusted.error.message}`,
|
|
295
|
+
},
|
|
296
|
+
requestId,
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (!trusted.value) {
|
|
301
|
+
c.status(statusFor('signer-not-trusted') as never);
|
|
302
|
+
return c.json(
|
|
303
|
+
toWireError(
|
|
304
|
+
{
|
|
305
|
+
code: 'signer-not-trusted',
|
|
306
|
+
message: `Signer key "${wire.signerKeyId as unknown as string}" is not on this tenant's trust list`,
|
|
307
|
+
signerKeyId: wire.signerKeyId as unknown as string,
|
|
308
|
+
},
|
|
309
|
+
requestId,
|
|
310
|
+
),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const canonicalEnvelope = canonicalizeEnvelope({
|
|
315
|
+
imageDigest,
|
|
316
|
+
artifactVersion: wire.artifactVersion,
|
|
317
|
+
indexHash: wire.indexHash,
|
|
318
|
+
tenantId: tenantId as unknown as string,
|
|
319
|
+
publishedAt: wire.publishedAt,
|
|
320
|
+
});
|
|
321
|
+
let signatureBytes: Buffer;
|
|
322
|
+
try {
|
|
323
|
+
signatureBytes = Buffer.from(wire.signature, 'base64');
|
|
324
|
+
} catch {
|
|
325
|
+
c.status(statusFor('signature-invalid') as never);
|
|
326
|
+
return c.json(
|
|
327
|
+
toWireError(
|
|
328
|
+
{ code: 'signature-invalid', message: 'signature is not valid base64' },
|
|
329
|
+
requestId,
|
|
330
|
+
),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const verification = verifyEd25519(
|
|
334
|
+
publicKeyBytes.value,
|
|
335
|
+
canonicalEnvelope,
|
|
336
|
+
new Uint8Array(signatureBytes),
|
|
337
|
+
);
|
|
338
|
+
if (verification.kind === 'err') {
|
|
339
|
+
c.status(statusFor('signature-invalid') as never);
|
|
340
|
+
return c.json(
|
|
341
|
+
toWireError(
|
|
342
|
+
{
|
|
343
|
+
code: 'signature-invalid',
|
|
344
|
+
message: `Signature verification rejected: ${verification.error.message}`,
|
|
345
|
+
},
|
|
346
|
+
requestId,
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (!verification.value) {
|
|
351
|
+
c.status(statusFor('signature-invalid') as never);
|
|
352
|
+
return c.json(
|
|
353
|
+
toWireError(
|
|
354
|
+
{
|
|
355
|
+
code: 'signature-invalid',
|
|
356
|
+
message:
|
|
357
|
+
'Ed25519 signature does not match canonical envelope { imageDigest, artifactVersion, indexHash, tenantId, publishedAt }',
|
|
358
|
+
},
|
|
359
|
+
requestId,
|
|
360
|
+
),
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ---- Image pullability + index integrity ----
|
|
365
|
+
const head = await bindings.imageRegistry.head({ tenantId, imageRef: wire.imageRef });
|
|
366
|
+
if (head.kind === 'err') {
|
|
367
|
+
c.status(statusFor('image-unverifiable') as never);
|
|
368
|
+
return c.json(
|
|
369
|
+
toWireError(
|
|
370
|
+
{
|
|
371
|
+
code: 'image-unverifiable',
|
|
372
|
+
message: `Image head probe failed: ${head.error.message}`,
|
|
373
|
+
imageRef: wire.imageRef,
|
|
374
|
+
cause: head.error.code,
|
|
375
|
+
},
|
|
376
|
+
requestId,
|
|
377
|
+
),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
if (head.value.digest !== imageDigest) {
|
|
381
|
+
c.status(statusFor('image-unverifiable') as never);
|
|
382
|
+
return c.json(
|
|
383
|
+
toWireError(
|
|
384
|
+
{
|
|
385
|
+
code: 'image-unverifiable',
|
|
386
|
+
message: `Registry digest ${head.value.digest} does not match imageRef digest ${imageDigest}`,
|
|
387
|
+
imageRef: wire.imageRef,
|
|
388
|
+
},
|
|
389
|
+
requestId,
|
|
390
|
+
),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const indexFile = await bindings.imageRegistry.extractFile({
|
|
395
|
+
tenantId,
|
|
396
|
+
imageRef: wire.imageRef,
|
|
397
|
+
path: '/app/index.json',
|
|
398
|
+
});
|
|
399
|
+
if (indexFile.kind === 'err') {
|
|
400
|
+
c.status(statusFor('image-unverifiable') as never);
|
|
401
|
+
return c.json(
|
|
402
|
+
toWireError(
|
|
403
|
+
{
|
|
404
|
+
code: 'image-unverifiable',
|
|
405
|
+
message: `Extract /app/index.json failed: ${indexFile.error.message}`,
|
|
406
|
+
imageRef: wire.imageRef,
|
|
407
|
+
cause: indexFile.error.code,
|
|
408
|
+
},
|
|
409
|
+
requestId,
|
|
410
|
+
),
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
const normalisedIndexHash = normaliseSha256Hex(indexFile.value.sha256);
|
|
414
|
+
const normalisedWireIndexHash = normaliseSha256Hex(wire.indexHash);
|
|
415
|
+
if (normalisedIndexHash !== normalisedWireIndexHash) {
|
|
416
|
+
c.status(statusFor('image-unverifiable') as never);
|
|
417
|
+
return c.json(
|
|
418
|
+
toWireError(
|
|
419
|
+
{
|
|
420
|
+
code: 'image-unverifiable',
|
|
421
|
+
message: `Image /app/index.json sha256 (${normalisedIndexHash}) does not match signed indexHash (${normalisedWireIndexHash})`,
|
|
422
|
+
imageRef: wire.imageRef,
|
|
423
|
+
},
|
|
424
|
+
requestId,
|
|
425
|
+
),
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// What registers is the image's own index, the bytes the signature
|
|
430
|
+
// covers (through indexHash), never a copy from the request.
|
|
431
|
+
const imageIndex = parseImageIndex(indexFile.value.bytes);
|
|
432
|
+
if (imageIndex === undefined) {
|
|
433
|
+
c.status(statusFor('image-unverifiable') as never);
|
|
434
|
+
return c.json(
|
|
435
|
+
toWireError(
|
|
436
|
+
{
|
|
437
|
+
code: 'image-unverifiable',
|
|
438
|
+
message: 'Image /app/index.json is not a JSON object',
|
|
439
|
+
imageRef: wire.imageRef,
|
|
440
|
+
},
|
|
441
|
+
requestId,
|
|
442
|
+
),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
if (imageIndex.artifactVersion !== wire.artifactVersion) {
|
|
446
|
+
c.status(statusFor('image-unverifiable') as never);
|
|
447
|
+
return c.json(
|
|
448
|
+
toWireError(
|
|
449
|
+
{
|
|
450
|
+
code: 'image-unverifiable',
|
|
451
|
+
message: `Image /app/index.json is artifact ${JSON.stringify(imageIndex.artifactVersion)}, not the signed ${wire.artifactVersion}`,
|
|
452
|
+
imageRef: wire.imageRef,
|
|
453
|
+
},
|
|
454
|
+
requestId,
|
|
455
|
+
),
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ---- Manifest validation ----
|
|
460
|
+
const validation = validatePrimitives(imageIndex, {
|
|
461
|
+
imageRef: wire.imageRef,
|
|
462
|
+
artifactVersion: wire.artifactVersion,
|
|
463
|
+
});
|
|
464
|
+
if (validation.kind === 'err') {
|
|
465
|
+
c.status(statusFor('deployment-validation-failed') as never);
|
|
466
|
+
return c.json(
|
|
467
|
+
toWireError(
|
|
468
|
+
{
|
|
469
|
+
code: 'deployment-validation-failed',
|
|
470
|
+
message: `Deployment manifest validation failed (${validation.error.details.length} issue${validation.error.details.length === 1 ? '' : 's'})`,
|
|
471
|
+
issues: validation.error.details as unknown as Record<string, unknown>[],
|
|
472
|
+
},
|
|
473
|
+
requestId,
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
const validated = validation.value;
|
|
478
|
+
|
|
479
|
+
// ---- Registry upserts (with rollback tracking) ----
|
|
480
|
+
//
|
|
481
|
+
// Agent-registry, flow-registry, tool-registry, and
|
|
482
|
+
// guardrail-registry writes are all project-scoped. All four loops resolve
|
|
483
|
+
// the tenant's Default project via `projectBinding.getDefault(
|
|
484
|
+
// tenantId)` once per request and thread the resolved id into
|
|
485
|
+
// `AgentRegistryBinding.publish(...)` /
|
|
486
|
+
// `FlowRegistryBinding.publish(...)` /
|
|
487
|
+
// `ToolRegistryBinding.register(...)` /
|
|
488
|
+
// `GuardrailRegistryBinding.register(...)`.
|
|
489
|
+
//
|
|
490
|
+
// Fail-fast: if a deployment brings any of those primitives but
|
|
491
|
+
// no `projectBinding` was wired, we surface `500 internal-server-
|
|
492
|
+
// error` with an operator-actionable message BEFORE any registry
|
|
493
|
+
// writes so rollback stays empty.
|
|
494
|
+
const needsProjectBinding =
|
|
495
|
+
validated.agents.length > 0 ||
|
|
496
|
+
validated.flows.length > 0 ||
|
|
497
|
+
validated.tools.length > 0 ||
|
|
498
|
+
validated.guardrails.length > 0;
|
|
499
|
+
if (needsProjectBinding && bindings.projectBinding === undefined) {
|
|
500
|
+
c.status(statusFor('internal-server-error') as never);
|
|
501
|
+
return c.json(
|
|
502
|
+
toWireError(
|
|
503
|
+
{
|
|
504
|
+
code: 'internal-server-error',
|
|
505
|
+
message:
|
|
506
|
+
'agent/flow/tool/guardrail publish requires projectBinding; deployment blocked. Wire projectBinding into DeploymentsRouterBindings at app-creation time.',
|
|
507
|
+
},
|
|
508
|
+
requestId,
|
|
509
|
+
),
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
let defaultProjectId: import('@kindgi/types').ProjectId | undefined;
|
|
513
|
+
if (needsProjectBinding && bindings.projectBinding !== undefined) {
|
|
514
|
+
const defaultProject = await bindings.projectBinding.getDefault(tenantId);
|
|
515
|
+
if (defaultProject === undefined) {
|
|
516
|
+
c.status(statusFor('internal-server-error') as never);
|
|
517
|
+
return c.json(
|
|
518
|
+
toWireError(
|
|
519
|
+
{
|
|
520
|
+
code: 'internal-server-error',
|
|
521
|
+
message:
|
|
522
|
+
'no Default project exists for this tenant; deployment blocked. Every tenant should auto-seed a Default project at creation.',
|
|
523
|
+
},
|
|
524
|
+
requestId,
|
|
525
|
+
),
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
defaultProjectId = defaultProject.id;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const rolled: RollbackAction[] = [];
|
|
532
|
+
try {
|
|
533
|
+
if (bindings.toolRegistry !== undefined && defaultProjectId !== undefined) {
|
|
534
|
+
for (const tool of validated.tools) {
|
|
535
|
+
const projectIdForTool = defaultProjectId;
|
|
536
|
+
const outcome = await bindings.toolRegistry.publish({
|
|
537
|
+
tenantId,
|
|
538
|
+
projectId: projectIdForTool,
|
|
539
|
+
tool,
|
|
540
|
+
enqueueTuples: (toolId) =>
|
|
541
|
+
tuplesForCreate({
|
|
542
|
+
kind: 'tool',
|
|
543
|
+
id: toolId as ToolId,
|
|
544
|
+
tenantId,
|
|
545
|
+
projectId: projectIdForTool,
|
|
546
|
+
}),
|
|
547
|
+
});
|
|
548
|
+
if (outcome.kind === 'ok') {
|
|
549
|
+
const toolId = outcome.toolId;
|
|
550
|
+
const version = outcome.version;
|
|
551
|
+
rolled.push(async () => {
|
|
552
|
+
await bindings.toolRegistry?.unregister({ tenantId, toolId, version });
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (bindings.guardrailRegistry !== undefined && defaultProjectId !== undefined) {
|
|
558
|
+
for (const guardrail of validated.guardrails) {
|
|
559
|
+
const projectIdForGuardrail = defaultProjectId;
|
|
560
|
+
const outcome = await bindings.guardrailRegistry.register({
|
|
561
|
+
tenantId,
|
|
562
|
+
projectId: projectIdForGuardrail,
|
|
563
|
+
guardrail,
|
|
564
|
+
enqueueTuples: (guardrailId) =>
|
|
565
|
+
tuplesForCreate({
|
|
566
|
+
kind: 'guardrail',
|
|
567
|
+
id: guardrailId as GuardrailId,
|
|
568
|
+
tenantId,
|
|
569
|
+
projectId: projectIdForGuardrail,
|
|
570
|
+
}),
|
|
571
|
+
});
|
|
572
|
+
if (outcome.kind === 'ok') {
|
|
573
|
+
const guardrailId = outcome.guardrailId;
|
|
574
|
+
rolled.push(async () => {
|
|
575
|
+
await bindings.guardrailRegistry?.unregister({ tenantId, guardrailId });
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (bindings.agentRegistry !== undefined && defaultProjectId !== undefined) {
|
|
581
|
+
for (const agent of validated.agents) {
|
|
582
|
+
const projectIdForAgent = defaultProjectId;
|
|
583
|
+
const outcome = await bindings.agentRegistry.publish({
|
|
584
|
+
tenantId,
|
|
585
|
+
projectId: projectIdForAgent,
|
|
586
|
+
agent,
|
|
587
|
+
// Signed deploys have no per-request principal — pass parent-
|
|
588
|
+
// only tuples (no owner grant). Tenant admins keep access via
|
|
589
|
+
// `admin from parent` cascade.
|
|
590
|
+
enqueueTuples: (agentId) =>
|
|
591
|
+
tuplesForCreate({
|
|
592
|
+
kind: 'agent',
|
|
593
|
+
id: agentId as AgentId,
|
|
594
|
+
tenantId,
|
|
595
|
+
projectId: projectIdForAgent,
|
|
596
|
+
}),
|
|
597
|
+
});
|
|
598
|
+
if (outcome.kind === 'ok') {
|
|
599
|
+
const agentId = outcome.agentId;
|
|
600
|
+
const version = outcome.version;
|
|
601
|
+
rolled.push(async () => {
|
|
602
|
+
await bindings.agentRegistry?.unregister({ tenantId, agentId, version });
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (bindings.flowRegistry !== undefined && defaultProjectId !== undefined) {
|
|
608
|
+
for (const flow of validated.flows) {
|
|
609
|
+
const projectIdForGraph = defaultProjectId;
|
|
610
|
+
const outcome = await bindings.flowRegistry.publish({
|
|
611
|
+
tenantId,
|
|
612
|
+
projectId: projectIdForGraph,
|
|
613
|
+
flow,
|
|
614
|
+
enqueueTuples: (flowId) =>
|
|
615
|
+
tuplesForCreate({
|
|
616
|
+
kind: 'flow',
|
|
617
|
+
id: flowId as FlowId,
|
|
618
|
+
tenantId,
|
|
619
|
+
projectId: projectIdForGraph,
|
|
620
|
+
}),
|
|
621
|
+
});
|
|
622
|
+
if (outcome.kind === 'ok') {
|
|
623
|
+
const flowId = outcome.flowId;
|
|
624
|
+
const version = outcome.version;
|
|
625
|
+
rolled.push(async () => {
|
|
626
|
+
await bindings.flowRegistry?.unregister({ tenantId, flowId, version });
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
} catch (cause) {
|
|
632
|
+
await rollback(rolled);
|
|
633
|
+
c.status(statusFor('internal-server-error') as never);
|
|
634
|
+
return c.json(
|
|
635
|
+
toWireError(
|
|
636
|
+
{
|
|
637
|
+
code: 'internal-server-error',
|
|
638
|
+
message: `Registry upsert failed: ${errMessage(cause)}`,
|
|
639
|
+
},
|
|
640
|
+
requestId,
|
|
641
|
+
),
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// ---- Deployment record ----
|
|
646
|
+
const primitives: DeploymentPrimitiveCounts = {
|
|
647
|
+
tools: validated.tools.length,
|
|
648
|
+
guardrails: validated.guardrails.length,
|
|
649
|
+
agents: validated.agents.length,
|
|
650
|
+
flows: validated.flows.length,
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
let outcome: DeploymentRegisterOutcome;
|
|
654
|
+
try {
|
|
655
|
+
outcome = await bindings.deploymentRegistry.register({
|
|
656
|
+
tenantId,
|
|
657
|
+
imageRef: wire.imageRef,
|
|
658
|
+
imageDigest,
|
|
659
|
+
artifactVersion: wire.artifactVersion,
|
|
660
|
+
indexHash: normalisedWireIndexHash,
|
|
661
|
+
signerKeyId: wire.signerKeyId,
|
|
662
|
+
signerPublicKey: publicKeyBase64,
|
|
663
|
+
signature: wire.signature,
|
|
664
|
+
publishedAt: wire.publishedAt,
|
|
665
|
+
primitives,
|
|
666
|
+
contents: {
|
|
667
|
+
tools: validated.tools.map((t) => ({
|
|
668
|
+
id: t.id as unknown as string,
|
|
669
|
+
version: t.version,
|
|
670
|
+
})),
|
|
671
|
+
guardrails: validated.guardrails.map((g) => ({ id: g.id as unknown as string })),
|
|
672
|
+
agents: validated.agents.map((a) => ({
|
|
673
|
+
id: a.id as unknown as string,
|
|
674
|
+
version: a.version as unknown as string,
|
|
675
|
+
})),
|
|
676
|
+
flows: validated.flows.map((f) => ({
|
|
677
|
+
id: f.id as unknown as string,
|
|
678
|
+
version: f.version as unknown as string,
|
|
679
|
+
})),
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
} catch (cause) {
|
|
683
|
+
await rollback(rolled);
|
|
684
|
+
c.status(statusFor('internal-server-error') as never);
|
|
685
|
+
return c.json(
|
|
686
|
+
toWireError(
|
|
687
|
+
{
|
|
688
|
+
code: 'internal-server-error',
|
|
689
|
+
message: `Deployment record failed: ${errMessage(cause)}`,
|
|
690
|
+
},
|
|
691
|
+
requestId,
|
|
692
|
+
),
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (outcome.kind === 'error') {
|
|
697
|
+
await rollback(rolled);
|
|
698
|
+
c.status(statusFor('internal-server-error') as never);
|
|
699
|
+
return c.json(
|
|
700
|
+
toWireError(
|
|
701
|
+
{
|
|
702
|
+
code: 'internal-server-error',
|
|
703
|
+
message: `Deployment record failed: ${outcome.message}`,
|
|
704
|
+
cause: outcome.code,
|
|
705
|
+
},
|
|
706
|
+
requestId,
|
|
707
|
+
),
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
if (outcome.kind === 'already-registered') {
|
|
711
|
+
// Digest-based idempotency: same digest already recorded. Roll back
|
|
712
|
+
// any freshly-inserted primitives (the primitive itself is fine —
|
|
713
|
+
// the previous deploy landed it — but re-inserting is idempotent
|
|
714
|
+
// at the primitive layer too, so `ok` outcomes only happen when
|
|
715
|
+
// the primitive was DIFFERENT this run, which is an inconsistent
|
|
716
|
+
// wire body).
|
|
717
|
+
await rollback(rolled);
|
|
718
|
+
c.status(200);
|
|
719
|
+
return c.json(serializeDeployment(outcome.deployment));
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
await notifyWrites(tenantId, validated, bindings);
|
|
723
|
+
c.status(201);
|
|
724
|
+
return c.json(serializeDeployment(outcome.deployment));
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
// ---------- POST /:deploymentId/secrets ----------
|
|
728
|
+
//
|
|
729
|
+
// Deployment-scoped bulk secret sync.
|
|
730
|
+
//
|
|
731
|
+
// Body shape:
|
|
732
|
+
// {
|
|
733
|
+
// envName: EnvName;
|
|
734
|
+
// secrets: Array<{ name, ref } | { name, value }>;
|
|
735
|
+
// }
|
|
736
|
+
//
|
|
737
|
+
// - `{ name, ref }` entries → validated via `SecretBinding.get`. No
|
|
738
|
+
// bytes cross the wire. Missing → 404 `secret-not-found` (matches
|
|
739
|
+
// the framework-wide `secret-not-found` → 404 mapping in
|
|
740
|
+
// `errors.ts`). The `ref` value is echoed
|
|
741
|
+
// verbatim in the response (opaque reference the deployment
|
|
742
|
+
// runtime resolves at dispatch).
|
|
743
|
+
// - `{ name, value }` entries → written via `SecretBinding.set` with
|
|
744
|
+
// `writeMode: 'add-version'` + `tags.deployment = deploymentId`. The
|
|
745
|
+
// response returns the just-minted reference (framework-generated
|
|
746
|
+
// `secret:` scheme so the developer can round-trip).
|
|
747
|
+
//
|
|
748
|
+
// Scope is derived from the target deployment record, NOT accepted from
|
|
749
|
+
// the wire (CRITICAL). The `Deployment` shape carries no `projectId`, so
|
|
750
|
+
// the scope is `{ kind: 'tenant', tenantId }`; `deriveScopeFromDeployment`
|
|
751
|
+
// switches to a project scope if a record does carry one.
|
|
752
|
+
//
|
|
753
|
+
// Capability: `secrets:write` (fail-closed).
|
|
754
|
+
// Idempotency-Key: honoured by the middleware layer.
|
|
755
|
+
r.post('/:deploymentId/secrets', async (c) => {
|
|
756
|
+
const requestId = c.get('requestId');
|
|
757
|
+
const tenantId = c.get('tenantId') as TenantId;
|
|
758
|
+
const deploymentId = c.req.param('deploymentId');
|
|
759
|
+
|
|
760
|
+
if (bindings.secretsBinding === undefined) {
|
|
761
|
+
c.status(statusFor('internal-server-error') as never);
|
|
762
|
+
return c.json(
|
|
763
|
+
toWireError(
|
|
764
|
+
{
|
|
765
|
+
code: 'internal-server-error',
|
|
766
|
+
message:
|
|
767
|
+
'secretsBinding is not wired; POST /v1/deployments/:deploymentId/secrets is unavailable. ' +
|
|
768
|
+
'Wire secretsBinding into DeploymentsRouterBindings.',
|
|
769
|
+
},
|
|
770
|
+
requestId,
|
|
771
|
+
),
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (!hasCapability(c, 'secrets:write')) {
|
|
776
|
+
c.status(statusFor('permission-denied') as never);
|
|
777
|
+
return c.json(
|
|
778
|
+
toWireError(
|
|
779
|
+
{
|
|
780
|
+
code: 'permission-denied',
|
|
781
|
+
message:
|
|
782
|
+
'Bearer token is missing the `secrets:write` capability required for deployment secret sync.',
|
|
783
|
+
},
|
|
784
|
+
requestId,
|
|
785
|
+
),
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Resolve the target deployment first — its `(tenantId, projectId?)`
|
|
790
|
+
// is what determines the write scope.
|
|
791
|
+
const deployment = await bindings.deploymentRegistry.get({ tenantId, deploymentId });
|
|
792
|
+
if (deployment === null) {
|
|
793
|
+
c.status(statusFor('deployment-not-found') as never);
|
|
794
|
+
return c.json(
|
|
795
|
+
toWireError(
|
|
796
|
+
{
|
|
797
|
+
code: 'deployment-not-found',
|
|
798
|
+
message: `No deployment with id "${deploymentId}" under this tenant`,
|
|
799
|
+
deploymentId,
|
|
800
|
+
},
|
|
801
|
+
requestId,
|
|
802
|
+
),
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
const scope = deriveScopeFromDeployment(deployment);
|
|
806
|
+
|
|
807
|
+
let body: unknown;
|
|
808
|
+
try {
|
|
809
|
+
body = await c.req.json();
|
|
810
|
+
} catch {
|
|
811
|
+
c.status(statusFor('bad-input') as never);
|
|
812
|
+
return c.json(
|
|
813
|
+
toWireError({ code: 'bad-input', message: 'Request body must be valid JSON' }, requestId),
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
817
|
+
c.status(statusFor('bad-input') as never);
|
|
818
|
+
return c.json(
|
|
819
|
+
toWireError(
|
|
820
|
+
{ code: 'bad-input', message: 'Request body must be a JSON object' },
|
|
821
|
+
requestId,
|
|
822
|
+
),
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
const b = body as Record<string, unknown>;
|
|
826
|
+
|
|
827
|
+
const envNameResult = requireEnvName(typeof b.envName === 'string' ? b.envName : undefined);
|
|
828
|
+
if (envNameResult.kind === 'err') {
|
|
829
|
+
c.status(statusFor(envNameResult.code) as never);
|
|
830
|
+
return c.json(
|
|
831
|
+
toWireError({ code: envNameResult.code, message: envNameResult.message }, requestId),
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
const envName = envNameResult.envName;
|
|
835
|
+
|
|
836
|
+
if (!Array.isArray(b.secrets)) {
|
|
837
|
+
c.status(statusFor('bad-input') as never);
|
|
838
|
+
return c.json(
|
|
839
|
+
toWireError(
|
|
840
|
+
{
|
|
841
|
+
code: 'bad-input',
|
|
842
|
+
message: '`secrets` must be an array of `{ name, ref }` or `{ name, value }` entries',
|
|
843
|
+
},
|
|
844
|
+
requestId,
|
|
845
|
+
),
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
// Empty array is legal (a no-op sync). Every real entry must be an
|
|
849
|
+
// object with a non-empty `name` and exactly one of `ref` / `value`.
|
|
850
|
+
const parsed: ParsedSyncEntry[] = [];
|
|
851
|
+
for (let i = 0; i < b.secrets.length; i++) {
|
|
852
|
+
const raw = b.secrets[i];
|
|
853
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
854
|
+
c.status(statusFor('bad-input') as never);
|
|
855
|
+
return c.json(
|
|
856
|
+
toWireError(
|
|
857
|
+
{
|
|
858
|
+
code: 'bad-input',
|
|
859
|
+
message: `secrets[${i}] must be an object`,
|
|
860
|
+
},
|
|
861
|
+
requestId,
|
|
862
|
+
),
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
const e = raw as Record<string, unknown>;
|
|
866
|
+
if (typeof e.name !== 'string' || e.name.length === 0) {
|
|
867
|
+
c.status(statusFor('bad-input') as never);
|
|
868
|
+
return c.json(
|
|
869
|
+
toWireError(
|
|
870
|
+
{
|
|
871
|
+
code: 'bad-input',
|
|
872
|
+
message: `secrets[${i}].name is required and must be a non-empty string`,
|
|
873
|
+
},
|
|
874
|
+
requestId,
|
|
875
|
+
),
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
const hasRef = typeof e.ref === 'string';
|
|
879
|
+
const hasValue = typeof e.value === 'string';
|
|
880
|
+
if (hasRef === hasValue) {
|
|
881
|
+
c.status(statusFor('bad-input') as never);
|
|
882
|
+
return c.json(
|
|
883
|
+
toWireError(
|
|
884
|
+
{
|
|
885
|
+
code: 'bad-input',
|
|
886
|
+
message: `secrets[${i}] must set exactly one of \`ref\` or \`value\``,
|
|
887
|
+
},
|
|
888
|
+
requestId,
|
|
889
|
+
),
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (hasRef) {
|
|
893
|
+
parsed.push({ kind: 'ref', name: e.name, ref: e.ref as string });
|
|
894
|
+
} else {
|
|
895
|
+
parsed.push({ kind: 'value', name: e.name, value: e.value as string });
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
let resolvedCount = 0;
|
|
900
|
+
let addedCount = 0;
|
|
901
|
+
const references: Array<{ name: string; ref: string; version: number }> = [];
|
|
902
|
+
for (const entry of parsed) {
|
|
903
|
+
if (entry.kind === 'ref') {
|
|
904
|
+
const rec = await bindings.secretsBinding.get({ scope, envName, name: entry.name });
|
|
905
|
+
if (rec === null || rec.revokedAt !== undefined) {
|
|
906
|
+
c.status(statusFor('secret-not-found') as never);
|
|
907
|
+
return c.json(
|
|
908
|
+
toWireError(
|
|
909
|
+
{
|
|
910
|
+
code: 'secret-not-found',
|
|
911
|
+
message: `secrets[${entry.name}]: no secret at (${scope.kind}, ${envName as unknown as string}, ${entry.name}) — write with { name, value } first or fix the ref.`,
|
|
912
|
+
name: entry.name,
|
|
913
|
+
},
|
|
914
|
+
requestId,
|
|
915
|
+
),
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
resolvedCount += 1;
|
|
919
|
+
references.push({ name: entry.name, ref: entry.ref, version: rec.currentVersion });
|
|
920
|
+
} else {
|
|
921
|
+
const outcome = await bindings.secretsBinding.set({
|
|
922
|
+
scope,
|
|
923
|
+
envName,
|
|
924
|
+
name: entry.name,
|
|
925
|
+
value: entry.value,
|
|
926
|
+
writeMode: 'add-version',
|
|
927
|
+
tags: { deployment: deploymentId },
|
|
928
|
+
// Only fires on create-new; here we're adding a version → no-op.
|
|
929
|
+
enqueueTuples: () => [],
|
|
930
|
+
});
|
|
931
|
+
if (outcome.kind !== 'ok') {
|
|
932
|
+
// add-version should never trigger `already-exists`; treat any
|
|
933
|
+
// non-ok as a hard failure (binding-side conflict, encryption
|
|
934
|
+
// failure, etc.). Fail the whole request (atomic model).
|
|
935
|
+
const code =
|
|
936
|
+
outcome.kind === 'version-conflict'
|
|
937
|
+
? 'secret-write-conflict'
|
|
938
|
+
: outcome.kind === 'already-exists'
|
|
939
|
+
? 'secret-write-conflict'
|
|
940
|
+
: outcome.code;
|
|
941
|
+
const message =
|
|
942
|
+
outcome.kind === 'version-conflict'
|
|
943
|
+
? `secrets[${entry.name}]: version conflict (stored=${outcome.currentVersion}); retry.`
|
|
944
|
+
: outcome.kind === 'already-exists'
|
|
945
|
+
? `secrets[${entry.name}]: already-exists surface on add-version (adapter bug); investigate.`
|
|
946
|
+
: `secrets[${entry.name}]: ${outcome.message}`;
|
|
947
|
+
c.status(statusFor(code) as never);
|
|
948
|
+
return c.json(toWireError({ code, message }, requestId));
|
|
949
|
+
}
|
|
950
|
+
addedCount += 1;
|
|
951
|
+
// Reference format: `secret:<envName>/<name>#<version>`. Opaque
|
|
952
|
+
// to the wire — the deployment runtime resolves via SecretBinding
|
|
953
|
+
// at dispatch time.
|
|
954
|
+
const ref = `secret:${envName as unknown as string}/${entry.name}#${outcome.versionId}`;
|
|
955
|
+
references.push({ name: entry.name, ref, version: outcome.versionId });
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return c.json({
|
|
960
|
+
resolved: resolvedCount,
|
|
961
|
+
added: addedCount,
|
|
962
|
+
references,
|
|
963
|
+
});
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
return r;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// ------------------------------------------------------------
|
|
970
|
+
// Scope derivation for deployment secret sync
|
|
971
|
+
// ------------------------------------------------------------
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Derive the write scope for a deployment secret-sync request from
|
|
975
|
+
* the deployment record itself. Never accepts a caller-supplied scope
|
|
976
|
+
* override on the wire (CRITICAL — scope MUST be inferred
|
|
977
|
+
* server-side to prevent tenant/project confusion attacks).
|
|
978
|
+
*
|
|
979
|
+
* The `Deployment` type carries no `projectId`, so this normally returns
|
|
980
|
+
* `{ kind: 'tenant', tenantId }`. If a binding's record does carry a
|
|
981
|
+
* non-empty `projectId`, the helper returns a project scope instead. The
|
|
982
|
+
* field is read dynamically because the type does not declare it.
|
|
983
|
+
*/
|
|
984
|
+
function deriveScopeFromDeployment(d: Deployment): Scope {
|
|
985
|
+
const maybeProjectId = (d as Deployment & { readonly projectId?: unknown }).projectId;
|
|
986
|
+
if (typeof maybeProjectId === 'string' && maybeProjectId.length > 0) {
|
|
987
|
+
return {
|
|
988
|
+
kind: 'project',
|
|
989
|
+
tenantId: d.tenantId,
|
|
990
|
+
projectId: maybeProjectId as unknown as ProjectId,
|
|
991
|
+
} as Scope;
|
|
992
|
+
}
|
|
993
|
+
return { kind: 'tenant', tenantId: d.tenantId } as Scope;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
type ParsedSyncEntry =
|
|
997
|
+
| { readonly kind: 'ref'; readonly name: string; readonly ref: string }
|
|
998
|
+
| { readonly kind: 'value'; readonly name: string; readonly value: string };
|
|
999
|
+
|
|
1000
|
+
// ------------------------------------------------------------
|
|
1001
|
+
// Wire body validation
|
|
1002
|
+
// ------------------------------------------------------------
|
|
1003
|
+
|
|
1004
|
+
interface WireBody {
|
|
1005
|
+
readonly imageRef: string;
|
|
1006
|
+
readonly artifactVersion: string;
|
|
1007
|
+
readonly indexHash: string;
|
|
1008
|
+
readonly signerKeyId: SigningKeyId;
|
|
1009
|
+
readonly signerPublicKey: string;
|
|
1010
|
+
readonly signature: string;
|
|
1011
|
+
readonly publishedAt: string;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
interface Issue {
|
|
1015
|
+
readonly path: string;
|
|
1016
|
+
readonly message: string;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
type ParseResult =
|
|
1020
|
+
| { readonly kind: 'ok'; readonly value: WireBody }
|
|
1021
|
+
| {
|
|
1022
|
+
readonly kind: 'err';
|
|
1023
|
+
readonly error: { readonly message: string; readonly issues: readonly Issue[] };
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
const ARTIFACT_VERSION_RE = /^\d{8}\.\d+$/;
|
|
1027
|
+
const SHA256_HEX_RE = /^[0-9a-f]{64}$/i;
|
|
1028
|
+
const SHA256_PREFIX_RE = /^sha256:[0-9a-f]{64}$/i;
|
|
1029
|
+
|
|
1030
|
+
function parseWireBody(body: unknown): ParseResult {
|
|
1031
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
1032
|
+
return {
|
|
1033
|
+
kind: 'err',
|
|
1034
|
+
error: {
|
|
1035
|
+
message: 'Request body must be a JSON object',
|
|
1036
|
+
issues: [{ path: '', message: 'must be an object' }],
|
|
1037
|
+
},
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
const b = body as Record<string, unknown>;
|
|
1041
|
+
const issues: Issue[] = [];
|
|
1042
|
+
const requireString = (field: string): string | undefined => {
|
|
1043
|
+
const raw = b[field];
|
|
1044
|
+
if (typeof raw !== 'string' || raw.length === 0) {
|
|
1045
|
+
issues.push({ path: field, message: `${field} must be a non-empty string` });
|
|
1046
|
+
return undefined;
|
|
1047
|
+
}
|
|
1048
|
+
return raw;
|
|
1049
|
+
};
|
|
1050
|
+
const imageRef = requireString('imageRef');
|
|
1051
|
+
const artifactVersion = requireString('artifactVersion');
|
|
1052
|
+
const indexHash = requireString('indexHash');
|
|
1053
|
+
const signerKeyId = requireString('signerKeyId');
|
|
1054
|
+
const signerPublicKey = requireString('signerPublicKey');
|
|
1055
|
+
const signature = requireString('signature');
|
|
1056
|
+
const publishedAt = requireString('publishedAt');
|
|
1057
|
+
|
|
1058
|
+
if (artifactVersion !== undefined && !ARTIFACT_VERSION_RE.test(artifactVersion)) {
|
|
1059
|
+
issues.push({
|
|
1060
|
+
path: 'artifactVersion',
|
|
1061
|
+
message: 'artifactVersion must match YYYYMMDD.N',
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
if (
|
|
1065
|
+
indexHash !== undefined &&
|
|
1066
|
+
!SHA256_PREFIX_RE.test(indexHash) &&
|
|
1067
|
+
!SHA256_HEX_RE.test(indexHash)
|
|
1068
|
+
) {
|
|
1069
|
+
issues.push({
|
|
1070
|
+
path: 'indexHash',
|
|
1071
|
+
message: 'indexHash must be sha256:<64-hex> (or bare 64-hex)',
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
if (publishedAt !== undefined && Number.isNaN(Date.parse(publishedAt))) {
|
|
1075
|
+
issues.push({ path: 'publishedAt', message: 'publishedAt must be ISO-8601' });
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (issues.length > 0) {
|
|
1079
|
+
return {
|
|
1080
|
+
kind: 'err',
|
|
1081
|
+
error: { message: issues[0]?.message ?? 'body validation failed', issues },
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
return {
|
|
1086
|
+
kind: 'ok',
|
|
1087
|
+
value: {
|
|
1088
|
+
imageRef: imageRef as string,
|
|
1089
|
+
artifactVersion: artifactVersion as string,
|
|
1090
|
+
indexHash: indexHash as string,
|
|
1091
|
+
signerKeyId: signerKeyId as unknown as SigningKeyId,
|
|
1092
|
+
signerPublicKey: signerPublicKey as string,
|
|
1093
|
+
signature: signature as string,
|
|
1094
|
+
publishedAt: publishedAt as string,
|
|
1095
|
+
},
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/** The image's `/app/index.json` as an object, or `undefined` when it isn't one. */
|
|
1100
|
+
function parseImageIndex(bytes: Uint8Array): Readonly<Record<string, unknown>> | undefined {
|
|
1101
|
+
let parsed: unknown;
|
|
1102
|
+
try {
|
|
1103
|
+
parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
1104
|
+
} catch {
|
|
1105
|
+
return undefined;
|
|
1106
|
+
}
|
|
1107
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
1108
|
+
? (parsed as Readonly<Record<string, unknown>>)
|
|
1109
|
+
: undefined;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function extractImageDigest(imageRef: string): string | null {
|
|
1113
|
+
const at = imageRef.lastIndexOf('@');
|
|
1114
|
+
if (at < 0) return null;
|
|
1115
|
+
const digest = imageRef.slice(at + 1);
|
|
1116
|
+
if (!SHA256_PREFIX_RE.test(digest)) return null;
|
|
1117
|
+
return digest.toLowerCase();
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function normaliseSha256Hex(raw: string): string {
|
|
1121
|
+
const lower = raw.toLowerCase();
|
|
1122
|
+
return lower.startsWith('sha256:') ? lower : `sha256:${lower}`;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// ------------------------------------------------------------
|
|
1126
|
+
// Canonical envelope
|
|
1127
|
+
// ------------------------------------------------------------
|
|
1128
|
+
|
|
1129
|
+
interface EnvelopeFields {
|
|
1130
|
+
readonly imageDigest: string;
|
|
1131
|
+
readonly artifactVersion: string;
|
|
1132
|
+
readonly indexHash: string;
|
|
1133
|
+
readonly tenantId: string;
|
|
1134
|
+
readonly publishedAt: string;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Canonical form of the signed envelope: JSON object with sorted keys,
|
|
1139
|
+
* no whitespace, UTF-8. Deterministic by construction — the same field
|
|
1140
|
+
* values always produce byte-identical output.
|
|
1141
|
+
*/
|
|
1142
|
+
function canonicalizeEnvelope(fields: EnvelopeFields): Uint8Array {
|
|
1143
|
+
const ordered: Record<string, string> = {
|
|
1144
|
+
artifactVersion: fields.artifactVersion,
|
|
1145
|
+
imageDigest: normaliseSha256Hex(fields.imageDigest),
|
|
1146
|
+
indexHash: normaliseSha256Hex(fields.indexHash),
|
|
1147
|
+
publishedAt: fields.publishedAt,
|
|
1148
|
+
tenantId: fields.tenantId,
|
|
1149
|
+
};
|
|
1150
|
+
const json = JSON.stringify(ordered);
|
|
1151
|
+
return new TextEncoder().encode(json);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// ------------------------------------------------------------
|
|
1155
|
+
// Primitive validation
|
|
1156
|
+
// ------------------------------------------------------------
|
|
1157
|
+
|
|
1158
|
+
interface ValidationDetail {
|
|
1159
|
+
readonly primitive: 'tool' | 'guardrail' | 'agent' | 'flow';
|
|
1160
|
+
readonly index: number;
|
|
1161
|
+
readonly id?: string;
|
|
1162
|
+
readonly path: string;
|
|
1163
|
+
readonly message: string;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
interface ValidatedPrimitives {
|
|
1167
|
+
readonly tools: readonly import('@kindgi/tools').ToolManifest[];
|
|
1168
|
+
readonly guardrails: readonly Guardrail[];
|
|
1169
|
+
readonly agents: readonly import('@kindgi/agents').Agent[];
|
|
1170
|
+
readonly flows: readonly import('@kindgi/flow').Flow[];
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
type ValidateResult =
|
|
1174
|
+
| { readonly kind: 'ok'; readonly value: ValidatedPrimitives }
|
|
1175
|
+
| { readonly kind: 'err'; readonly error: { readonly details: readonly ValidationDetail[] } };
|
|
1176
|
+
|
|
1177
|
+
/** The image a deployment's code lives in, for each primitive's `codeArtifactRef`. */
|
|
1178
|
+
interface DeployedImage {
|
|
1179
|
+
readonly imageRef: string;
|
|
1180
|
+
readonly artifactVersion: string;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/** The pointer to a tool's or guardrail's module inside the deployed image. */
|
|
1184
|
+
function ociCodeArtifactRef(image: DeployedImage, modulePath: unknown) {
|
|
1185
|
+
return typeof modulePath === 'string' && modulePath.length > 0
|
|
1186
|
+
? {
|
|
1187
|
+
codeArtifactRef: {
|
|
1188
|
+
kind: 'oci' as const,
|
|
1189
|
+
imageRef: image.imageRef,
|
|
1190
|
+
modulePath,
|
|
1191
|
+
artifactVersion: image.artifactVersion,
|
|
1192
|
+
},
|
|
1193
|
+
}
|
|
1194
|
+
: {};
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Validate the index's primitives as the registries store them. Tools
|
|
1199
|
+
* and guardrails keep where their code is (`codeArtifactRef`, an `oci`
|
|
1200
|
+
* pointer into the deployed image) in place of the index's module path;
|
|
1201
|
+
* a guardrail's `checkId` becomes its `check`, as `kindgi dev`'s disk
|
|
1202
|
+
* registry maps it.
|
|
1203
|
+
*/
|
|
1204
|
+
function validatePrimitives(
|
|
1205
|
+
index: Readonly<Record<string, unknown>>,
|
|
1206
|
+
image: DeployedImage,
|
|
1207
|
+
): ValidateResult {
|
|
1208
|
+
const details: ValidationDetail[] = [];
|
|
1209
|
+
|
|
1210
|
+
const tools = readArray(index.tools);
|
|
1211
|
+
const guardrails = readArray(index.guardrails);
|
|
1212
|
+
const agents = readArray(index.agents);
|
|
1213
|
+
const flows = readArray(index.flows);
|
|
1214
|
+
|
|
1215
|
+
const validatedTools: import('@kindgi/tools').ToolManifest[] = [];
|
|
1216
|
+
tools.forEach((raw, i) => {
|
|
1217
|
+
if (raw === null || typeof raw !== 'object') {
|
|
1218
|
+
details.push({ primitive: 'tool', index: i, path: '', message: 'must be an object' });
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
const { modulePath, ...manifest } = raw as Record<string, unknown>;
|
|
1222
|
+
const r = validateToolManifest({ ...manifest, ...ociCodeArtifactRef(image, modulePath) });
|
|
1223
|
+
if (r.kind === 'err') {
|
|
1224
|
+
const err = r.error as {
|
|
1225
|
+
message: string;
|
|
1226
|
+
issues?: readonly { path: string; message: string }[];
|
|
1227
|
+
};
|
|
1228
|
+
const rawId = (raw as { id?: unknown }).id;
|
|
1229
|
+
const idBase: Partial<ValidationDetail> = typeof rawId === 'string' ? { id: rawId } : {};
|
|
1230
|
+
if (err.issues && err.issues.length > 0) {
|
|
1231
|
+
for (const issue of err.issues) {
|
|
1232
|
+
details.push({
|
|
1233
|
+
primitive: 'tool',
|
|
1234
|
+
index: i,
|
|
1235
|
+
...idBase,
|
|
1236
|
+
path: issue.path,
|
|
1237
|
+
message: issue.message,
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
} else {
|
|
1241
|
+
details.push({ primitive: 'tool', index: i, ...idBase, path: '', message: err.message });
|
|
1242
|
+
}
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
validatedTools.push(r.value);
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
const validatedGuardrails: Guardrail[] = [];
|
|
1249
|
+
guardrails.forEach((raw, i) => {
|
|
1250
|
+
if (raw === null || typeof raw !== 'object') {
|
|
1251
|
+
details.push({ primitive: 'guardrail', index: i, path: '', message: 'must be an object' });
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const {
|
|
1255
|
+
checkModulePath,
|
|
1256
|
+
checkId,
|
|
1257
|
+
configSchema: _configSchema,
|
|
1258
|
+
...rest
|
|
1259
|
+
} = raw as Record<string, unknown>;
|
|
1260
|
+
const r = validateGuardrailSpec({
|
|
1261
|
+
...rest,
|
|
1262
|
+
check: checkId ?? rest.check ?? rest.id,
|
|
1263
|
+
...ociCodeArtifactRef(image, checkModulePath),
|
|
1264
|
+
});
|
|
1265
|
+
if (r.kind === 'err') {
|
|
1266
|
+
const err = r.error as {
|
|
1267
|
+
message: string;
|
|
1268
|
+
issues?: readonly { path: string; message: string }[];
|
|
1269
|
+
};
|
|
1270
|
+
const rawId = (raw as { id?: unknown }).id;
|
|
1271
|
+
const idBase: Partial<ValidationDetail> = typeof rawId === 'string' ? { id: rawId } : {};
|
|
1272
|
+
if (err.issues && err.issues.length > 0) {
|
|
1273
|
+
for (const issue of err.issues) {
|
|
1274
|
+
details.push({
|
|
1275
|
+
primitive: 'guardrail',
|
|
1276
|
+
index: i,
|
|
1277
|
+
...idBase,
|
|
1278
|
+
path: issue.path,
|
|
1279
|
+
message: issue.message,
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
} else {
|
|
1283
|
+
details.push({
|
|
1284
|
+
primitive: 'guardrail',
|
|
1285
|
+
index: i,
|
|
1286
|
+
...idBase,
|
|
1287
|
+
path: '',
|
|
1288
|
+
message: err.message,
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
validatedGuardrails.push(r.value);
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
const validatedAgents: import('@kindgi/agents').Agent[] = [];
|
|
1297
|
+
agents.forEach((raw, i) => {
|
|
1298
|
+
if (raw === null || typeof raw !== 'object') {
|
|
1299
|
+
details.push({ primitive: 'agent', index: i, path: '', message: 'must be an object' });
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
const { modulePath: _mp, ...spec } = raw as Record<string, unknown>;
|
|
1303
|
+
const shapeIssues = validateAgentIndexShape(spec);
|
|
1304
|
+
if (shapeIssues.length > 0) {
|
|
1305
|
+
const rawId = (spec as { id?: unknown }).id;
|
|
1306
|
+
const idBase: Partial<ValidationDetail> = typeof rawId === 'string' ? { id: rawId } : {};
|
|
1307
|
+
for (const issue of shapeIssues) {
|
|
1308
|
+
details.push({ primitive: 'agent', index: i, ...idBase, ...issue });
|
|
1309
|
+
}
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
validatedAgents.push(spec as unknown as import('@kindgi/agents').Agent);
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
const validatedGraphs: import('@kindgi/flow').Flow[] = [];
|
|
1316
|
+
flows.forEach((raw, i) => {
|
|
1317
|
+
if (raw === null || typeof raw !== 'object') {
|
|
1318
|
+
details.push({ primitive: 'flow', index: i, path: '', message: 'must be an object' });
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const { modulePath: _mp, kernelPayloadVersion: _kpv, ...spec } = raw as Record<string, unknown>;
|
|
1322
|
+
const shapeIssues = validateFlowIndexShape(spec);
|
|
1323
|
+
if (shapeIssues.length > 0) {
|
|
1324
|
+
const rawId = (spec as { id?: unknown }).id;
|
|
1325
|
+
const idBase: Partial<ValidationDetail> = typeof rawId === 'string' ? { id: rawId } : {};
|
|
1326
|
+
for (const issue of shapeIssues) {
|
|
1327
|
+
details.push({ primitive: 'flow', index: i, ...idBase, ...issue });
|
|
1328
|
+
}
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
validatedGraphs.push(spec as unknown as import('@kindgi/flow').Flow);
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
if (details.length > 0) {
|
|
1335
|
+
return { kind: 'err', error: { details } };
|
|
1336
|
+
}
|
|
1337
|
+
return {
|
|
1338
|
+
kind: 'ok',
|
|
1339
|
+
value: {
|
|
1340
|
+
tools: validatedTools,
|
|
1341
|
+
guardrails: validatedGuardrails,
|
|
1342
|
+
agents: validatedAgents,
|
|
1343
|
+
flows: validatedGraphs,
|
|
1344
|
+
},
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function readArray(value: unknown): readonly unknown[] {
|
|
1349
|
+
return Array.isArray(value) ? (value as readonly unknown[]) : [];
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Minimal shape check on an `IndexedAgent` after `modulePath` is
|
|
1356
|
+
* stripped. Deeper validation (RetrievalIntent shape, budget arithmetic,
|
|
1357
|
+
* conversation policy tri-state) is left to the runtime at dispatch —
|
|
1358
|
+
* the deploy route only validates that the wire is well-shaped enough to
|
|
1359
|
+
* persist. The alternative is running `defineAgent`, but that requires
|
|
1360
|
+
* promoting `capabilities`/`tools` to their branded shapes; the pattern
|
|
1361
|
+
* across the API package is to keep transport shape checks lightweight
|
|
1362
|
+
* and leave semantic checks to the runtime.
|
|
1363
|
+
*/
|
|
1364
|
+
function validateAgentIndexShape(
|
|
1365
|
+
spec: Record<string, unknown>,
|
|
1366
|
+
): readonly { path: string; message: string }[] {
|
|
1367
|
+
const out: { path: string; message: string }[] = [];
|
|
1368
|
+
if (typeof spec.id !== 'string' || (spec.id as string).length === 0) {
|
|
1369
|
+
out.push({ path: '/id', message: 'agent.id must be a non-empty string' });
|
|
1370
|
+
}
|
|
1371
|
+
if (typeof spec.version !== 'string' || !SEMVER_RE.test(spec.version as string)) {
|
|
1372
|
+
out.push({ path: '/version', message: 'agent.version must be semver' });
|
|
1373
|
+
}
|
|
1374
|
+
if (typeof spec.name !== 'string' || (spec.name as string).length === 0) {
|
|
1375
|
+
out.push({ path: '/name', message: 'agent.name must be a non-empty string' });
|
|
1376
|
+
}
|
|
1377
|
+
if (typeof spec.instructions !== 'string' || (spec.instructions as string).length === 0) {
|
|
1378
|
+
out.push({ path: '/instructions', message: 'agent.instructions must be a non-empty string' });
|
|
1379
|
+
}
|
|
1380
|
+
if (!Array.isArray(spec.capabilities)) {
|
|
1381
|
+
out.push({ path: '/capabilities', message: 'agent.capabilities must be an array' });
|
|
1382
|
+
}
|
|
1383
|
+
if (!Array.isArray(spec.tools)) {
|
|
1384
|
+
out.push({ path: '/tools', message: 'agent.tools must be an array of tool ids' });
|
|
1385
|
+
}
|
|
1386
|
+
return out;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function validateFlowIndexShape(
|
|
1390
|
+
spec: Record<string, unknown>,
|
|
1391
|
+
): readonly { path: string; message: string }[] {
|
|
1392
|
+
const out: { path: string; message: string }[] = [];
|
|
1393
|
+
if (typeof spec.id !== 'string' || (spec.id as string).length === 0) {
|
|
1394
|
+
out.push({ path: '/id', message: 'flow.id must be a non-empty string' });
|
|
1395
|
+
}
|
|
1396
|
+
if (typeof spec.version !== 'string' || !SEMVER_RE.test(spec.version as string)) {
|
|
1397
|
+
out.push({ path: '/version', message: 'flow.version must be semver' });
|
|
1398
|
+
}
|
|
1399
|
+
if (!Array.isArray(spec.nodes)) {
|
|
1400
|
+
out.push({ path: '/nodes', message: 'flow.nodes must be an array' });
|
|
1401
|
+
}
|
|
1402
|
+
if (!Array.isArray(spec.edges)) {
|
|
1403
|
+
out.push({ path: '/edges', message: 'flow.edges must be an array' });
|
|
1404
|
+
}
|
|
1405
|
+
return out;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// ------------------------------------------------------------
|
|
1409
|
+
// Rollback / serialisation
|
|
1410
|
+
// ------------------------------------------------------------
|
|
1411
|
+
|
|
1412
|
+
type RollbackAction = () => Promise<void>;
|
|
1413
|
+
|
|
1414
|
+
async function rollback(actions: readonly RollbackAction[]): Promise<void> {
|
|
1415
|
+
// Reverse order — last-in, first-out — so registrations are undone in
|
|
1416
|
+
// the mirror sequence of their creation.
|
|
1417
|
+
for (let i = actions.length - 1; i >= 0; i--) {
|
|
1418
|
+
const action = actions[i];
|
|
1419
|
+
if (action === undefined) continue;
|
|
1420
|
+
try {
|
|
1421
|
+
await action();
|
|
1422
|
+
} catch {
|
|
1423
|
+
// Swallow: rollback is best-effort. Real production adapters
|
|
1424
|
+
// record a rollback-failure event so operators can reconcile.
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function serializeDeployment(d: Deployment): Record<string, unknown> {
|
|
1430
|
+
return {
|
|
1431
|
+
deploymentId: d.deploymentId,
|
|
1432
|
+
tenantId: d.tenantId as unknown as string,
|
|
1433
|
+
imageRef: d.imageRef,
|
|
1434
|
+
imageDigest: d.imageDigest,
|
|
1435
|
+
artifactVersion: d.artifactVersion,
|
|
1436
|
+
indexHash: d.indexHash,
|
|
1437
|
+
signerKeyId: d.signerKeyId as unknown as string,
|
|
1438
|
+
signerPublicKey: d.signerPublicKey,
|
|
1439
|
+
signature: d.signature,
|
|
1440
|
+
publishedAt: d.publishedAt,
|
|
1441
|
+
activatedAt: d.activatedAt,
|
|
1442
|
+
primitives: {
|
|
1443
|
+
tools: d.primitives.tools,
|
|
1444
|
+
guardrails: d.primitives.guardrails,
|
|
1445
|
+
agents: d.primitives.agents,
|
|
1446
|
+
flows: d.primitives.flows,
|
|
1447
|
+
},
|
|
1448
|
+
contents: d.contents,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/** Tell the catalog caches about what a new deployment registered (advisory). */
|
|
1453
|
+
async function notifyWrites(
|
|
1454
|
+
tenantId: TenantId,
|
|
1455
|
+
validated: ValidatedPrimitives,
|
|
1456
|
+
bindings: DeploymentsRouterBindings,
|
|
1457
|
+
): Promise<void> {
|
|
1458
|
+
for (const tool of validated.tools) {
|
|
1459
|
+
try {
|
|
1460
|
+
await bindings.onToolWrite?.({
|
|
1461
|
+
tenantId,
|
|
1462
|
+
toolId: tool.id as ToolId,
|
|
1463
|
+
version: tool.version,
|
|
1464
|
+
kind: 'publish',
|
|
1465
|
+
});
|
|
1466
|
+
} catch {
|
|
1467
|
+
// Advisory, as on `POST /v1/tools`.
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
for (const guardrail of validated.guardrails) {
|
|
1471
|
+
try {
|
|
1472
|
+
await bindings.onGuardrailWrite?.({
|
|
1473
|
+
tenantId,
|
|
1474
|
+
guardrailId: guardrail.id as GuardrailId,
|
|
1475
|
+
kind: 'register',
|
|
1476
|
+
});
|
|
1477
|
+
} catch {
|
|
1478
|
+
// Advisory, as on `POST /v1/guardrails`.
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function errMessage(cause: unknown): string {
|
|
1484
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Formats a sha256 digest in the canonical `sha256:<hex>` shape. Not used
|
|
1488
|
+
// by the router or elsewhere in this package; exported from this module
|
|
1489
|
+
// only.
|
|
1490
|
+
export function sha256HexPrefixed(bytes: Uint8Array): string {
|
|
1491
|
+
const hex = createHash('sha256').update(bytes).digest('hex');
|
|
1492
|
+
return `sha256:${hex}`;
|
|
1493
|
+
}
|