@codemation/host 0.8.0 → 0.9.1

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.
Files changed (148) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/{ApiPaths-Dv1dcHu_.js → ApiPaths-DCvrlIjg.js} +12 -1
  3. package/dist/{ApiPaths-Dv1dcHu_.js.map → ApiPaths-DCvrlIjg.js.map} +1 -1
  4. package/dist/{AppConfigFactory-Cx4qQvRk.js → AppConfigFactory-D4LL1aOR.js} +77 -297
  5. package/dist/AppConfigFactory-D4LL1aOR.js.map +1 -0
  6. package/dist/{AppConfigFactory-BT0y0LVC.d.ts → AppConfigFactory-DncmwCD1.d.ts} +2918 -199
  7. package/dist/{AppContainerFactory-DRTjG7nG.js → AppContainerFactory-CHCXP2rn.js} +1735 -474
  8. package/dist/AppContainerFactory-CHCXP2rn.js.map +1 -0
  9. package/dist/{CodemationAppContext-CGFYVcSb.d.ts → CodemationAppContext-K51b7oXe.d.ts} +3 -3
  10. package/dist/{CodemationAuthoring.types-DiKKogum.d.ts → CodemationAuthoring.types-BXlXIl4K.d.ts} +4 -4
  11. package/dist/{CodemationConfigNormalizer-48f-T66P.d.ts → CodemationConfigNormalizer-B4rDYC9h.d.ts} +3 -3
  12. package/dist/{CodemationConsumerConfigLoader-_PIYqwVx.d.ts → CodemationConsumerConfigLoader-Dt4jyLx6.d.ts} +2 -2
  13. package/dist/{CodemationPluginListMerger-DP7djJ9S.d.ts → CodemationPluginListMerger-DS6I3Xe0.d.ts} +24 -12
  14. package/dist/{persistenceServer-C-hH4z6l.js → CodemationPostgresPrismaClientFactory-C7156Fe-.js} +2 -2
  15. package/dist/CodemationPostgresPrismaClientFactory-C7156Fe-.js.map +1 -0
  16. package/dist/CodemationPostgresPrismaClientFactory-CTNTPnDr.d.ts +9 -0
  17. package/dist/{CredentialContractsRegistry-Bq2bq28t.d.ts → CredentialContractsRegistry-Dgu-rEXi.d.ts} +16 -3
  18. package/dist/{CredentialServices-BLloBztI.d.ts → CredentialServices-B3wPyp2y.d.ts} +4 -4
  19. package/dist/{CredentialServices-Dk8yypeL.js → CredentialServices-Bios0dM8.js} +10 -4
  20. package/dist/CredentialServices-Bios0dM8.js.map +1 -0
  21. package/dist/{InternalHonoApiRouteRegistrar-c7t3KnV_.d.ts → InternalHonoApiRouteRegistrar-Ce1yxpnO.d.ts} +1 -1
  22. package/dist/{InternalPingRegistrar-DY3kSfxP.js → InternalPingRegistrar-BavAAnvk.js} +19 -16
  23. package/dist/InternalPingRegistrar-BavAAnvk.js.map +1 -0
  24. package/dist/{ItemsInputNormalizer-_RwIfRIQ.d.ts → ItemsInputNormalizer-CFkfNMLt.d.ts} +1434 -1225
  25. package/dist/PrismaMigrationDeployer-DdEcXXVi.d.ts +14 -0
  26. package/dist/{PublicFrontendBootstrapFactory-Dv04tJ-6.d.ts → PublicFrontendBootstrapFactory-ClEjZP74.d.ts} +2 -2
  27. package/dist/{PublicFrontendBootstrapJsonCodec-CXG9Dxft.d.ts → PublicFrontendBootstrapJsonCodec-HNItQ7ol.d.ts} +6 -1
  28. package/dist/{TelemetryContracts-BtDx84Cp.d.ts → TelemetryContracts-DpZEODQM.d.ts} +2 -2
  29. package/dist/{WorkflowPolicyUiPresentationFactory-6MyjCvBO.d.ts → WorkflowPolicyUiPresentationFactory-BNn2fvR_.d.ts} +2 -2
  30. package/dist/{WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js → WorkflowPolicyUiPresentationFactory-DfvD2VHk.js} +1 -1
  31. package/dist/{WorkflowPolicyUiPresentationFactory-Bb-ae_Zh.js.map → WorkflowPolicyUiPresentationFactory-DfvD2VHk.js.map} +1 -1
  32. package/dist/authoring.d.ts +4 -4
  33. package/dist/client.d.ts +1 -1
  34. package/dist/client.js +1 -1
  35. package/dist/consumer.d.ts +5 -5
  36. package/dist/credentials.d.ts +5 -5
  37. package/dist/credentials.js +1 -1
  38. package/dist/devServerSidecar.d.ts +2 -2
  39. package/dist/dto.d.ts +5 -5
  40. package/dist/{index-DilAYwnH.d.ts → index-ChIfeWzk.d.ts} +71 -28
  41. package/dist/index.d.ts +17 -16
  42. package/dist/index.js +8 -8
  43. package/dist/infrastructure/persistence/PrismaMigrationOperations.d.ts +44 -0
  44. package/dist/infrastructure/persistence/PrismaMigrationOperations.js +302 -0
  45. package/dist/infrastructure/persistence/PrismaMigrationOperations.js.map +1 -0
  46. package/dist/mapping.d.ts +2 -2
  47. package/dist/mapping.js +1 -1
  48. package/dist/nextServer.d.ts +15 -13
  49. package/dist/nextServer.js +6 -6
  50. package/dist/pairing.d.ts +28 -9
  51. package/dist/pairing.js +19 -3
  52. package/dist/pairing.js.map +1 -0
  53. package/dist/{pairing.types-snfZ_OzB.d.ts → pairing.types-D9Bjn98U.d.ts} +1 -1
  54. package/dist/persistenceServer.d.ts +31 -7
  55. package/dist/persistenceServer.js +2 -2
  56. package/dist/{server-09PKasWR.d.ts → server-B5trn7y4.d.ts} +5 -5
  57. package/dist/{server-vtRCPgRJ.js → server-CNj_y0QO.js} +4 -4
  58. package/dist/{server-vtRCPgRJ.js.map → server-CNj_y0QO.js.map} +1 -1
  59. package/dist/server.d.ts +10 -10
  60. package/dist/server.js +8 -8
  61. package/package.json +11 -10
  62. package/playwright.config.ts +8 -2
  63. package/playwright.scaffolded-dev.config.ts +8 -2
  64. package/prisma/migrations/20260526120000_credential_material_pointer/migration.sql +18 -0
  65. package/prisma/migrations/20260527120000_add_human_task/migration.sql +32 -0
  66. package/prisma/migrations/20260527130000_add_hitl_state_json/migration.sql +6 -0
  67. package/prisma/migrations/20260527130000_add_hmac_nonce/migration.sql +12 -0
  68. package/prisma/migrations.sqlite/20260526120000_credential_material_pointer/migration.sql +13 -0
  69. package/prisma/migrations.sqlite/20260527120000_add_human_task/migration.sql +30 -0
  70. package/prisma/migrations.sqlite/20260527130000_add_hitl_state_json/migration.sql +6 -0
  71. package/prisma/migrations.sqlite/20260527130000_add_hmac_nonce/migration.sql +9 -0
  72. package/prisma/schema.postgresql.prisma +48 -0
  73. package/prisma/schema.sqlite.prisma +48 -0
  74. package/prisma-generated/prisma-postgresql-client/edge.js +40 -6
  75. package/prisma-generated/prisma-postgresql-client/index-browser.js +36 -2
  76. package/prisma-generated/prisma-postgresql-client/index.d.ts +3179 -163
  77. package/prisma-generated/prisma-postgresql-client/index.js +40 -6
  78. package/prisma-generated/prisma-postgresql-client/package.json +1 -1
  79. package/prisma-generated/prisma-postgresql-client/schema.prisma +48 -0
  80. package/prisma-generated/prisma-sqlite-client/edge.js +40 -6
  81. package/prisma-generated/prisma-sqlite-client/index-browser.js +36 -2
  82. package/prisma-generated/prisma-sqlite-client/index.d.ts +3175 -163
  83. package/prisma-generated/prisma-sqlite-client/index.js +40 -6
  84. package/prisma-generated/prisma-sqlite-client/package.json +1 -1
  85. package/prisma-generated/prisma-sqlite-client/schema.prisma +48 -0
  86. package/src/application/contracts/CredentialContractsRegistry.ts +15 -0
  87. package/src/application/credentials/AppGalleryProjector.ts +69 -0
  88. package/src/application/hitl/DecideHumanTaskCommandHandler.ts +149 -0
  89. package/src/application/hitl/DecisionSchemaValidator.ts +22 -0
  90. package/src/application/hitl/HitlCallbackHandler.ts +96 -0
  91. package/src/application/mapping/WorkflowDefinitionMapper.ts +1 -3
  92. package/src/application/queries/CredentialQueryHandlers.ts +2 -0
  93. package/src/application/queries/GetCredentialAppsQuery.ts +4 -0
  94. package/src/application/queries/GetCredentialAppsQueryHandler.ts +27 -0
  95. package/src/application/telemetry/ResumeTelemetryContextForRun.ts +53 -0
  96. package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +9 -8
  97. package/src/applicationTokens.ts +11 -1
  98. package/src/auth/managed/ManagedCorsMiddleware.ts +20 -5
  99. package/src/bootstrap/AppContainerFactory.ts +100 -0
  100. package/src/credentials/CachingCredentialMaterialProvider.ts +96 -0
  101. package/src/credentials/CompositeCredentialMaterialProvider.ts +47 -0
  102. package/src/credentials/ControlPlaneCatalogFetcher.ts +4 -24
  103. package/src/credentials/ControlPlaneCredentialMaterialProvider.ts +79 -0
  104. package/src/credentials/CredentialOAuth2MaterialReader.ts +2 -7
  105. package/src/credentials/InternalCredentialsBindingRegistrar.ts +83 -0
  106. package/src/credentials/LocalCredentialMaterialProvider.ts +92 -0
  107. package/src/domain/credentials/CredentialInstanceService.ts +5 -1
  108. package/src/domain/credentials/CredentialTypeRegistryImpl.ts +18 -4
  109. package/src/domain/workflows/WorkflowActivationPreflightRules.ts +7 -4
  110. package/src/dto.ts +2 -0
  111. package/src/hitl/ControlPlaneInboxChannel.ts +102 -0
  112. package/src/hitl/HitlResumeTokenSigner.ts +80 -0
  113. package/src/hitl/HitlTimeoutJobScheduler.ts +89 -0
  114. package/src/hitl/HitlTimeoutWorker.ts +143 -0
  115. package/src/hitl/InboxChannelResolver.ts +49 -0
  116. package/src/hitl/LocalInboxChannel.ts +37 -0
  117. package/src/infrastructure/persistence/PrismaCredentialStore.ts +10 -0
  118. package/src/infrastructure/persistence/PrismaHmacNonceStore.ts +29 -0
  119. package/src/infrastructure/persistence/PrismaHumanTaskStore.ts +156 -0
  120. package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +53 -383
  121. package/src/infrastructure/persistence/PrismaMigrationOperations.ts +401 -0
  122. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +39 -0
  123. package/src/mcp/AgentMcpIntegrationImpl.ts +5 -1
  124. package/src/pairing/HmacNonceStore.ts +14 -0
  125. package/src/pairing/HmacNonceStoreToken.ts +4 -0
  126. package/src/pairing/HmacRequestSigner.ts +10 -1
  127. package/src/pairing/InMemoryHmacNonceStore.ts +24 -0
  128. package/src/pairing/IncomingHmacVerifier.ts +28 -12
  129. package/src/pairing/InternalHmacAuthMiddleware.ts +1 -1
  130. package/src/pairing/index.ts +3 -0
  131. package/src/presentation/http/ApiPaths.ts +14 -0
  132. package/src/presentation/http/hono/HonoHttpAnonymousRoutePolicyRegistry.ts +4 -0
  133. package/src/presentation/http/hono/registrars/CredentialHonoApiRouteRegistrar.ts +1 -0
  134. package/src/presentation/http/hono/registrars/HitlDecideHonoApiRouteRegistrar.ts +54 -0
  135. package/src/presentation/http/hono/registrars/HitlInternalCallbackHonoApiRouteRegistrar.ts +33 -0
  136. package/src/presentation/http/hono/registrars/HitlResumeHonoApiRouteRegistrar.ts +43 -0
  137. package/src/presentation/http/routeHandlers/CredentialHttpRouteHandler.ts +9 -0
  138. package/src/presentation/http/routeHandlers/OAuth2HttpRouteHandlerFactory.ts +1 -1
  139. package/src/server.ts +7 -2
  140. package/src/workflows/InternalWorkflowTestRunRegistrar.ts +9 -0
  141. package/tsconfig.json +1 -0
  142. package/dist/AppConfigFactory-Cx4qQvRk.js.map +0 -1
  143. package/dist/AppContainerFactory-DRTjG7nG.js.map +0 -1
  144. package/dist/CredentialServices-Dk8yypeL.js.map +0 -1
  145. package/dist/InternalPingRegistrar-DY3kSfxP.js.map +0 -1
  146. package/dist/persistenceServer-B71RGvSj.d.ts +0 -30
  147. package/dist/persistenceServer-C-hH4z6l.js.map +0 -1
  148. package/src/credentials/catalogTypes.ts +0 -4
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "prisma-client-f1f592cca96cc07ff4c49ca6848788c0583fceb20133a8099c5c0d76247b55cc",
2
+ "name": "prisma-client-27202846702650939c7c4ff50ce95d5243619cf55a343054d3ecccc77fc95642",
3
3
  "main": "index.js",
4
4
  "types": "index.d.ts",
5
5
  "browser": "default.js",
@@ -22,6 +22,7 @@ model Run {
22
22
  policySnapshotJson String? @map("policy_snapshot_json")
23
23
  engineCountersJson String? @map("engine_counters_json")
24
24
  mutableStateJson String? @map("mutable_state_json")
25
+ hitlStateJson String? @map("hitl_state_json")
25
26
  outputsByNodeJson String @map("outputs_by_node_json")
26
27
  updatedAt String @map("updated_at")
27
28
  testSuiteRunId String? @map("test_suite_run_id")
@@ -346,6 +347,10 @@ model CredentialInstance {
346
347
  setupStatus String @map("setup_status")
347
348
  createdAt String @map("created_at")
348
349
  updatedAt String @map("updated_at")
350
+ // Material provider seam — see docs/design/credentials-oauth-unification.md.
351
+ // Pointer to where the bytes live (workspace DB vs control plane).
352
+ materialSource String @default("local") @map("material_source")
353
+ materialRef String @default("") @map("material_ref")
349
354
  }
350
355
 
351
356
  model CredentialSecretMaterial {
@@ -508,3 +513,46 @@ model WorkflowAuditLog {
508
513
  @@index([workflowId, occurredAt])
509
514
  @@map("workflow_audit_log")
510
515
  }
516
+
517
+ /// HMAC nonce store for replay protection (T6 security fix).
518
+ /// Nonces are persisted across process restarts so a replayed request within
519
+ /// the 300-second timestamp window is rejected even after a restart.
520
+ model HmacNonce {
521
+ nonce String @id @map("nonce")
522
+ expiresAt DateTime @map("expires_at")
523
+
524
+ @@index([expiresAt])
525
+ @@map("hmac_nonce")
526
+ }
527
+
528
+ model HumanTask {
529
+ id String @id @map("id")
530
+ runId String @map("run_id")
531
+ workflowId String @map("workflow_id")
532
+ workspaceId String? @map("workspace_id")
533
+ nodeId String @map("node_id")
534
+ activationId String @map("activation_id")
535
+ itemIndex Int @map("item_index")
536
+ /// pending | decided | timed_out | auto_accepted | cancelled
537
+ status String @map("status")
538
+ /// local | control-plane-inbox
539
+ channel String @map("channel")
540
+ subjectJson String @map("subject_json")
541
+ metadataJson String @map("metadata_json")
542
+ decisionSchemaJson String @map("decision_schema_json")
543
+ decisionSchemaHash String @map("decision_schema_hash")
544
+ /// halt | auto-accept
545
+ onTimeout String @map("on_timeout")
546
+ deliveryRefJson String? @map("delivery_ref_json")
547
+ decisionJson String? @map("decision_json")
548
+ decidedAt DateTime? @map("decided_at")
549
+ decidedByJson String? @map("decided_by_json")
550
+ resumeTokenHash String @map("resume_token_hash")
551
+ expiresAt DateTime @map("expires_at")
552
+ createdAt DateTime @default(now()) @map("created_at")
553
+
554
+ @@index([runId])
555
+ @@index([workflowId, status])
556
+ @@index([workspaceId, status, expiresAt])
557
+ @@map("human_task")
558
+ }
@@ -81,6 +81,21 @@ export type UpsertCredentialBindingRequest = Readonly<{
81
81
  instanceId: CredentialInstanceId;
82
82
  }>;
83
83
 
84
+ export type AppGalleryEntry = Readonly<{
85
+ mcpId: string;
86
+ displayName: string;
87
+ description: string;
88
+ iconUrl: string | null;
89
+ acceptedCredentialTypes: ReadonlyArray<string>;
90
+ primaryOAuthTypeId: string | null;
91
+ instances: ReadonlyArray<CredentialInstanceDto>;
92
+ }>;
93
+
94
+ export type AppsResponse = Readonly<{
95
+ apps: ReadonlyArray<AppGalleryEntry>;
96
+ customInstances: ReadonlyArray<CredentialInstanceDto>;
97
+ }>;
98
+
84
99
  export class CredentialResponseMapper {
85
100
  static toTypeDefinitionList(types: ReadonlyArray<CredentialTypeDefinition>): ReadonlyArray<CredentialTypeDefinition> {
86
101
  return [...types];
@@ -0,0 +1,69 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import type { McpServerDeclaration } from "@codemation/core";
3
+ import type { AppGalleryEntry, AppsResponse, CredentialInstanceDto } from "../contracts/CredentialContractsRegistry";
4
+ import { CredentialTypeRegistryImpl } from "../../domain/credentials/CredentialServices";
5
+
6
+ /**
7
+ * Produces gallery-ready DTOs by joining MCP server declarations × credential
8
+ * type registry × workspace credential instances.
9
+ *
10
+ * D2 rule: an instance belongs under a tile when its typeId is in
11
+ * mcp.acceptedCredentialTypes. Instances with no MCP home land in customInstances.
12
+ * An instance can appear under multiple tiles if more than one MCP accepts its type.
13
+ *
14
+ * D4 rule: primaryOAuthTypeId = first acceptedCredentialType whose
15
+ * CredentialTypeDefinition.auth.kind === "oauth2". null when none found.
16
+ *
17
+ * D5 rule: when mcpServers is null (unpaired), apps is [] and all instances are custom.
18
+ */
19
+ @injectable()
20
+ export class AppGalleryProjector {
21
+ constructor(
22
+ @inject(CredentialTypeRegistryImpl)
23
+ private readonly credentialTypeRegistry: CredentialTypeRegistryImpl,
24
+ ) {}
25
+
26
+ project(
27
+ mcpServers: ReadonlyArray<McpServerDeclaration> | null,
28
+ instances: ReadonlyArray<CredentialInstanceDto>,
29
+ ): AppsResponse {
30
+ if (mcpServers === null) {
31
+ return { apps: [], customInstances: [...instances] };
32
+ }
33
+
34
+ const mcpInstancedSetIds = new Set<string>();
35
+
36
+ const apps: AppGalleryEntry[] = mcpServers.map((mcp) => {
37
+ const accepted = mcp.acceptedCredentialTypes ?? [];
38
+ const acceptedSet = new Set(accepted);
39
+ const mcpInstances = instances.filter((i) => acceptedSet.has(i.typeId));
40
+ for (const i of mcpInstances) {
41
+ mcpInstancedSetIds.add(i.instanceId);
42
+ }
43
+ const primaryOAuthTypeId = this.findPrimaryOAuthTypeId(accepted);
44
+ return {
45
+ mcpId: mcp.id,
46
+ displayName: mcp.displayName,
47
+ description: mcp.description,
48
+ iconUrl: null,
49
+ acceptedCredentialTypes: [...accepted],
50
+ primaryOAuthTypeId,
51
+ instances: mcpInstances,
52
+ };
53
+ });
54
+
55
+ const customInstances = instances.filter((i) => !mcpInstancedSetIds.has(i.instanceId));
56
+
57
+ return { apps, customInstances };
58
+ }
59
+
60
+ private findPrimaryOAuthTypeId(acceptedTypes: ReadonlyArray<string>): string | null {
61
+ for (const typeId of acceptedTypes) {
62
+ const credType = this.credentialTypeRegistry.getCredentialType(typeId);
63
+ if (credType?.definition.auth?.kind === "oauth2") {
64
+ return typeId;
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+ }
@@ -0,0 +1,149 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import type { HumanTaskActor, HumanTaskStore, JsonValue } from "@codemation/core";
3
+ import { CodemationTelemetryAttributeNames, HumanTaskStoreToken } from "@codemation/core";
4
+ import { Engine } from "@codemation/core/bootstrap";
5
+ import { ApplicationRequestError } from "../ApplicationRequestError";
6
+ import { HitlResumeTokenSigner } from "../../hitl/HitlResumeTokenSigner";
7
+ import { HitlTimeoutJobScheduler } from "../../hitl/HitlTimeoutJobScheduler";
8
+ import { DecisionSchemaValidator } from "./DecisionSchemaValidator";
9
+ import { ResumeTelemetryContextForRun } from "../telemetry/ResumeTelemetryContextForRun";
10
+
11
+ export interface DecideHumanTaskArgs {
12
+ taskId: string;
13
+ decision: JsonValue;
14
+ decidedBy: HumanTaskActor;
15
+ }
16
+
17
+ export interface DecideHumanTaskResult {
18
+ status: "decided";
19
+ runStatus: "running" | "halted";
20
+ }
21
+
22
+ @injectable()
23
+ export class DecideHumanTaskCommandHandler {
24
+ private readonly taskStore: HumanTaskStore | undefined;
25
+
26
+ constructor(
27
+ @inject(HumanTaskStoreToken) taskStore: HumanTaskStore | undefined,
28
+ @inject(Engine) private readonly engine: Engine,
29
+ @inject(HitlResumeTokenSigner) private readonly tokenSigner: HitlResumeTokenSigner,
30
+ @inject(HitlTimeoutJobScheduler) private readonly timeoutScheduler: HitlTimeoutJobScheduler,
31
+ @inject(DecisionSchemaValidator) private readonly schemaValidator: DecisionSchemaValidator,
32
+ @inject(ResumeTelemetryContextForRun) private readonly resumeTelemetry: ResumeTelemetryContextForRun,
33
+ ) {
34
+ this.taskStore = taskStore;
35
+ }
36
+
37
+ async decide(args: DecideHumanTaskArgs): Promise<DecideHumanTaskResult> {
38
+ if (!this.taskStore) {
39
+ throw new ApplicationRequestError(503, "HITL is not available in this configuration");
40
+ }
41
+ const task = await this.taskStore.findById(args.taskId);
42
+ if (!task) {
43
+ throw new ApplicationRequestError(404, "HumanTask not found");
44
+ }
45
+
46
+ if (task.status !== "pending") {
47
+ throw new ApplicationRequestError(409, `HumanTask is not pending (current status: ${task.status})`);
48
+ }
49
+
50
+ // Validate decision body against the stored schema
51
+ const validationResult = this.schemaValidator.validate({
52
+ schemaJson: task.decisionSchemaJson,
53
+ value: args.decision,
54
+ });
55
+ if (!validationResult.valid) {
56
+ throw new ApplicationRequestError(
57
+ 422,
58
+ `Decision does not match the expected schema: ${validationResult.message}`,
59
+ );
60
+ }
61
+
62
+ const decidedAt = new Date();
63
+ await this.taskStore.markDecided({
64
+ taskId: args.taskId,
65
+ decision: args.decision,
66
+ decidedBy: args.decidedBy,
67
+ decidedAt,
68
+ });
69
+
70
+ // Cancel the timeout job to prevent double-resolution
71
+ await this.timeoutScheduler.cancelTimeoutJob(args.taskId);
72
+
73
+ // Emit hitl.task.decided on the run's trace.
74
+ const telemetry = await this.resumeTelemetry.forTask(args.taskId);
75
+ const latencyMs = decidedAt.getTime() - task.createdAt.getTime();
76
+ const decisionPayload = args.decision as Record<string, unknown> | null;
77
+ const decisionStatus =
78
+ typeof decisionPayload?.["approved"] === "boolean"
79
+ ? decisionPayload["approved"]
80
+ ? "approved"
81
+ : "rejected"
82
+ : "decided";
83
+ await telemetry?.addSpanEvent({
84
+ name: "hitl.task.decided",
85
+ attributes: {
86
+ [CodemationTelemetryAttributeNames.hitlTaskId]: args.taskId,
87
+ [CodemationTelemetryAttributeNames.hitlDecisionStatus]: decisionStatus,
88
+ actor: args.decidedBy.actorId,
89
+ latencyMs,
90
+ },
91
+ });
92
+
93
+ // Resume the suspended run
94
+ const resumeResult = await this.engine.resumeRun({
95
+ runId: task.runId,
96
+ taskId: task.id,
97
+ resumeContext: {
98
+ decision: {
99
+ kind: "decided",
100
+ value: args.decision,
101
+ actor: args.decidedBy,
102
+ decidedAt,
103
+ },
104
+ delivery: task.deliveryRef ?? null,
105
+ task: {
106
+ taskId: task.id,
107
+ runId: task.runId,
108
+ nodeId: task.nodeId,
109
+ expiresAt: task.expiresAt,
110
+ resumeUrl: "",
111
+ },
112
+ },
113
+ });
114
+
115
+ return {
116
+ status: "decided",
117
+ runStatus: resumeResult.status === "failed" || resumeResult.status === "halted" ? "halted" : "running",
118
+ };
119
+ }
120
+
121
+ /** Used by the token-authenticated resume endpoint to validate the token and extract the actor. */
122
+ async validateResumeToken(args: { taskId: string; token: string }): Promise<{ schemaHash: string }> {
123
+ const result = this.tokenSigner.verify(args.token);
124
+ if (!result.ok) {
125
+ if (result.reason === "expired") {
126
+ throw new ApplicationRequestError(410, "Resume token has expired");
127
+ }
128
+ throw new ApplicationRequestError(401, "Invalid resume token");
129
+ }
130
+ if (result.taskId !== args.taskId) {
131
+ throw new ApplicationRequestError(401, "Token taskId does not match");
132
+ }
133
+
134
+ if (!this.taskStore) {
135
+ throw new ApplicationRequestError(503, "HITL is not available in this configuration");
136
+ }
137
+ const task = await this.taskStore.findById(args.taskId);
138
+ if (!task) {
139
+ throw new ApplicationRequestError(404, "HumanTask not found");
140
+ }
141
+
142
+ // Schema hash drift detection (D6): token was signed with a hash of the schema at creation
143
+ if (task.decisionSchemaHash.slice(0, 8) !== result.schemaHash) {
144
+ throw new ApplicationRequestError(410, "Schema has changed since this token was issued");
145
+ }
146
+
147
+ return { schemaHash: result.schemaHash };
148
+ }
149
+ }
@@ -0,0 +1,22 @@
1
+ import Ajv from "ajv/dist/2020.js";
2
+ import { injectable } from "@codemation/core";
3
+ import type { JsonValue } from "@codemation/core";
4
+
5
+ // Zod v4's z.toJSONSchema() emits draft 2020-12, so we must use Ajv's 2020 build.
6
+ const ajv = new Ajv({ strict: false });
7
+
8
+ /**
9
+ * Validates a HITL decision payload against the JSON Schema that was recorded
10
+ * when the human task was created.
11
+ */
12
+ @injectable()
13
+ export class DecisionSchemaValidator {
14
+ validate(args: { schemaJson: string; value: JsonValue }): { valid: true } | { valid: false; message: string } {
15
+ const schema = JSON.parse(args.schemaJson) as object;
16
+ const fn = ajv.compile(schema);
17
+ if (fn(args.value)) {
18
+ return { valid: true };
19
+ }
20
+ return { valid: false, message: ajv.errorsText(fn.errors) };
21
+ }
22
+ }
@@ -0,0 +1,96 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import type { HumanTaskStore, JsonValue } from "@codemation/core";
3
+ import { HumanTaskStoreToken } from "@codemation/core";
4
+ import type { Logger } from "../logging/Logger";
5
+ import type { PairingConfig } from "../../pairing/pairing.types";
6
+ import { PairingConfigToken } from "../../pairing/PairingConfigToken";
7
+ import { ServerLoggerFactory } from "../../infrastructure/logging/ServerLoggerFactory";
8
+ import { DecideHumanTaskCommandHandler } from "./DecideHumanTaskCommandHandler";
9
+ import { ApplicationRequestError } from "../ApplicationRequestError";
10
+
11
+ export type HitlCallbackBody =
12
+ | { kind: "timeout" }
13
+ | {
14
+ decision: JsonValue;
15
+ actor?: { actorId: string; displayName?: string };
16
+ };
17
+
18
+ export type HitlCallbackResult =
19
+ | { status: 200; body: { ok: true } }
20
+ | { status: 400 | 403 | 404 | 409 | 422 | 503; body: { error: string } };
21
+
22
+ /**
23
+ * Handler for inbound HITL decision callbacks from the control plane.
24
+ *
25
+ * Validates the callback body, checks workspace identity, and delegates to
26
+ * `DecideHumanTaskCommandHandler` for the actual decision/timeout logic.
27
+ *
28
+ * Workspace identity is asserted via `PairingConfig.workspaceId` — the HMAC
29
+ * middleware already guarantees the request is signed by the paired CP, so
30
+ * this is a secondary assertion matching the task's stored workspace.
31
+ *
32
+ * The framework's timeout worker and CP's callback can both fire for the
33
+ * same task. Whichever lands first wins; the second gets a 409 from
34
+ * `markDecided`/`markTimedOut` (task already resolved). This is intentional.
35
+ */
36
+ @injectable()
37
+ export class HitlCallbackHandler {
38
+ private readonly logger: Logger;
39
+
40
+ constructor(
41
+ @inject(HumanTaskStoreToken) private readonly taskStore: HumanTaskStore | undefined,
42
+ @inject(PairingConfigToken) private readonly pairingConfig: PairingConfig,
43
+ @inject(DecideHumanTaskCommandHandler) private readonly decideHandler: DecideHumanTaskCommandHandler,
44
+ @inject(ServerLoggerFactory) loggerFactory: ServerLoggerFactory,
45
+ ) {
46
+ this.logger = loggerFactory.create("codemation.hitl.callback");
47
+ }
48
+
49
+ async handle(taskId: string, body: HitlCallbackBody): Promise<HitlCallbackResult> {
50
+ if (!this.taskStore) {
51
+ return { status: 503, body: { error: "HITL is not available in this configuration" } };
52
+ }
53
+
54
+ const task = await this.taskStore.findById(taskId);
55
+ if (!task) {
56
+ return { status: 404, body: { error: "HumanTask not found" } };
57
+ }
58
+
59
+ // Assert workspace identity: only the CP paired to this workspace may call back
60
+ if (task.workspaceId !== undefined && task.workspaceId !== this.pairingConfig.workspaceId) {
61
+ this.logger.warn(
62
+ `HITL callback workspace mismatch — taskId=${taskId} taskWorkspaceId=${task.workspaceId} pairingWorkspaceId=${this.pairingConfig.workspaceId}`,
63
+ );
64
+ return { status: 403, body: { error: "Workspace mismatch" } };
65
+ }
66
+
67
+ if (task.status !== "pending") {
68
+ return { status: 409, body: { error: `HumanTask is not pending (current status: ${task.status})` } };
69
+ }
70
+
71
+ // Timeout path (CP-originated timeout)
72
+ if ("kind" in body && body.kind === "timeout") {
73
+ await this.taskStore.markTimedOut(taskId);
74
+ this.logger.info(`HITL task timed out via CP callback — taskId=${taskId}`);
75
+ return { status: 200, body: { ok: true } };
76
+ }
77
+
78
+ const decisionBody = body as { decision: JsonValue; actor?: { actorId: string; displayName?: string } };
79
+
80
+ try {
81
+ await this.decideHandler.decide({
82
+ taskId,
83
+ decision: decisionBody.decision,
84
+ decidedBy: decisionBody.actor ?? { actorId: "cp-reviewer" },
85
+ });
86
+ } catch (err) {
87
+ if (err instanceof ApplicationRequestError) {
88
+ return { status: err.status as 400 | 403 | 404 | 409 | 422 | 503, body: { error: err.message } };
89
+ }
90
+ throw err;
91
+ }
92
+
93
+ this.logger.info(`HITL task decided via CP callback — taskId=${taskId}`);
94
+ return { status: 200, body: { ok: true } };
95
+ }
96
+ }
@@ -226,9 +226,7 @@ export class WorkflowDefinitionMapper implements DataMapper<WorkflowDefinition,
226
226
  if (!AgentConfigInspector.isAgentNodeConfig(node.config)) {
227
227
  continue;
228
228
  }
229
- const descriptors = AgentConnectionNodeCollector.collect(node.id, node.config, (id) =>
230
- this.mcpCatalog.get(id),
231
- );
229
+ const descriptors = AgentConnectionNodeCollector.collect(node.id, node.config, (id) => this.mcpCatalog.get(id));
232
230
  byAgentNodeId.set(node.id, descriptors);
233
231
  const byChildId = new Map<string, AgentConnectionNodeDescriptor>();
234
232
  for (const descriptor of descriptors) {
@@ -10,3 +10,5 @@ export { ListCredentialTypesQuery } from "./ListCredentialTypesQuery";
10
10
  export { ListCredentialTypesQueryHandler } from "./ListCredentialTypesQueryHandler";
11
11
  export { GetCredentialFieldEnvStatusQuery } from "./GetCredentialFieldEnvStatusQuery";
12
12
  export { GetCredentialFieldEnvStatusQueryHandler } from "./GetCredentialFieldEnvStatusQueryHandler";
13
+ export { GetCredentialAppsQuery } from "./GetCredentialAppsQuery";
14
+ export { GetCredentialAppsQueryHandler } from "./GetCredentialAppsQueryHandler";
@@ -0,0 +1,4 @@
1
+ import type { AppsResponse } from "../contracts/CredentialContractsRegistry";
2
+ import { Query } from "../bus/Query";
3
+
4
+ export class GetCredentialAppsQuery extends Query<AppsResponse> {}
@@ -0,0 +1,27 @@
1
+ import { inject } from "@codemation/core";
2
+ import { QueryHandler } from "../bus/QueryHandler";
3
+ import { HandlesQuery } from "../../infrastructure/di/HandlesQueryRegistry";
4
+ import type { AppsResponse } from "../contracts/CredentialContractsRegistry";
5
+ import { GetCredentialAppsQuery } from "./GetCredentialAppsQuery";
6
+ import { CredentialInstanceService } from "../../domain/credentials/CredentialInstanceService";
7
+ import { AppGalleryProjector } from "../credentials/AppGalleryProjector";
8
+ import { ControlPlaneCatalogFetcher } from "../../credentials/ControlPlaneCatalogFetcher";
9
+
10
+ @HandlesQuery.for(GetCredentialAppsQuery)
11
+ export class GetCredentialAppsQueryHandler extends QueryHandler<GetCredentialAppsQuery, AppsResponse> {
12
+ constructor(
13
+ @inject(CredentialInstanceService)
14
+ private readonly credentialInstanceService: CredentialInstanceService,
15
+ @inject(AppGalleryProjector)
16
+ private readonly appGalleryProjector: AppGalleryProjector,
17
+ @inject(ControlPlaneCatalogFetcher)
18
+ private readonly catalogFetcher: ControlPlaneCatalogFetcher,
19
+ ) {
20
+ super();
21
+ }
22
+
23
+ async execute(): Promise<AppsResponse> {
24
+ const instances = await this.credentialInstanceService.listInstances();
25
+ return this.appGalleryProjector.project(this.catalogFetcher.mcpServers, instances);
26
+ }
27
+ }
@@ -0,0 +1,53 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import type { ExecutionTelemetry } from "@codemation/core";
3
+ import type { HumanTaskStore } from "@codemation/core";
4
+ import { HumanTaskStoreToken } from "@codemation/core";
5
+ import { OtelExecutionTelemetryFactory } from "./OtelExecutionTelemetryFactory";
6
+
7
+ /**
8
+ * Reconstructs an {@link ExecutionTelemetry} scope for a run that is resuming from a
9
+ * `pending` HITL state.
10
+ *
11
+ * **Trace context note:** When the decide/timeout/cancel endpoint fires, the original
12
+ * run's OTel trace context is not available (different HTTP request, no propagation header).
13
+ * The `traceId` and root `spanId` are re-derived deterministically from `runId` using the
14
+ * same {@link OtelIdentityFactory} hashing that was used at run-start time. This means the
15
+ * span events emitted here (`hitl.task.decided`, `hitl.task.timed_out`, `hitl.task.cancelled`)
16
+ * are correctly routed into the run's existing trace tree in the span store — the join is
17
+ * by `traceId` / `runId`, not by a live in-process scope.
18
+ *
19
+ * If a future requirement calls for full W3C trace-context propagation across the
20
+ * suspend/resume boundary, store `traceId` + parent `spanId` on the `HumanTask` row and
21
+ * restore them here instead of re-deriving.
22
+ */
23
+ @injectable()
24
+ export class ResumeTelemetryContextForRun {
25
+ private readonly taskStore: HumanTaskStore | undefined;
26
+
27
+ constructor(
28
+ @inject(OtelExecutionTelemetryFactory) private readonly telemetryFactory: OtelExecutionTelemetryFactory,
29
+ @inject(HumanTaskStoreToken) taskStore: HumanTaskStore | undefined,
30
+ ) {
31
+ this.taskStore = taskStore;
32
+ }
33
+
34
+ /**
35
+ * Returns an {@link ExecutionTelemetry} scope keyed to the run's trace, or `undefined`
36
+ * when the task store is not available or the task is not found.
37
+ *
38
+ * Loads `workflowId` from the task record so callers don't need to look it up separately.
39
+ */
40
+ async forTask(taskId: string): Promise<ExecutionTelemetry | undefined> {
41
+ if (!this.taskStore) {
42
+ return undefined;
43
+ }
44
+ const task = await this.taskStore.findById(taskId);
45
+ if (!task) {
46
+ return undefined;
47
+ }
48
+ return this.telemetryFactory.create({
49
+ runId: task.runId,
50
+ workflowId: task.workflowId,
51
+ });
52
+ }
53
+ }
@@ -19,26 +19,27 @@ export class TelemetryRetentionTimestampFactory {
19
19
 
20
20
  createArtifactExpiry(policySnapshot: PersistedRunPolicySnapshot | undefined, observedAt: Date): string {
21
21
  return this.createExpiry(
22
- policySnapshot?.telemetryArtifactRetentionSeconds ?? TelemetryRetentionTimestampFactory.defaultArtifactRetentionSeconds,
22
+ policySnapshot?.telemetryArtifactRetentionSeconds ??
23
+ TelemetryRetentionTimestampFactory.defaultArtifactRetentionSeconds,
23
24
  observedAt,
24
25
  );
25
26
  }
26
27
 
27
28
  createMetricExpiry(policySnapshot: PersistedRunPolicySnapshot | undefined, observedAt: Date): string {
28
29
  return this.createExpiry(
29
- policySnapshot?.telemetryMetricRetentionSeconds ?? TelemetryRetentionTimestampFactory.defaultMetricRetentionSeconds,
30
+ policySnapshot?.telemetryMetricRetentionSeconds ??
31
+ TelemetryRetentionTimestampFactory.defaultMetricRetentionSeconds,
30
32
  observedAt,
31
33
  );
32
34
  }
33
35
 
34
- createTraceContextExpiry(
35
- policySnapshot: PersistedRunPolicySnapshot | undefined,
36
- observedAt: Date,
37
- ): string {
36
+ createTraceContextExpiry(policySnapshot: PersistedRunPolicySnapshot | undefined, observedAt: Date): string {
38
37
  const candidates = [
39
38
  policySnapshot?.telemetrySpanRetentionSeconds ?? TelemetryRetentionTimestampFactory.defaultSpanRetentionSeconds,
40
- policySnapshot?.telemetryArtifactRetentionSeconds ?? TelemetryRetentionTimestampFactory.defaultArtifactRetentionSeconds,
41
- policySnapshot?.telemetryMetricRetentionSeconds ?? TelemetryRetentionTimestampFactory.defaultMetricRetentionSeconds,
39
+ policySnapshot?.telemetryArtifactRetentionSeconds ??
40
+ TelemetryRetentionTimestampFactory.defaultArtifactRetentionSeconds,
41
+ policySnapshot?.telemetryMetricRetentionSeconds ??
42
+ TelemetryRetentionTimestampFactory.defaultMetricRetentionSeconds,
42
43
  ].filter((value): value is number => typeof value === "number" && value > 0);
43
44
  const maxSeconds = Math.max(...candidates);
44
45
  return this.createExpiry(maxSeconds, observedAt);
@@ -1,4 +1,4 @@
1
- import type { Clock, OAuthFlowExecutor, TypeToken } from "@codemation/core";
1
+ import type { Clock, CredentialMaterialProvider, OAuthFlowExecutor, TypeToken } from "@codemation/core";
2
2
  import type { SessionVerifier } from "./application/auth/SessionVerifier";
3
3
  import type { Command } from "./application/bus/Command";
4
4
  import type { CommandBus } from "./application/bus/CommandBus";
@@ -106,4 +106,14 @@ export const ApplicationTokens = {
106
106
  WorkflowAuditEmitter: Symbol.for("codemation.application.WorkflowAuditEmitter") as TypeToken<IWorkflowAuditEmitter>,
107
107
  ProcessRunner: Symbol.for("codemation.application.ProcessRunner") as TypeToken<ProcessRunner>,
108
108
  OAuthFlowExecutor: Symbol.for("codemation.application.OAuthFlowExecutor") as TypeToken<OAuthFlowExecutor>,
109
+ /**
110
+ * The provider that `CachingCredentialMaterialProvider` wraps. Bound to
111
+ * `LocalCredentialMaterialProvider` in standalone mode and to
112
+ * `CompositeCredentialMaterialProvider` in managed mode (which dispatches
113
+ * by `ref.source`). See `packages/host/src/credentials/` and
114
+ * `planning/sprints/credentials-vault/02-controlplane-material-provider.md`.
115
+ */
116
+ CredentialMaterialInnerProvider: Symbol.for(
117
+ "codemation.application.CredentialMaterialInnerProvider",
118
+ ) as TypeToken<CredentialMaterialProvider>,
109
119
  } as const;
@@ -4,12 +4,27 @@ import type { MiddlewareHandler } from "hono";
4
4
  /**
5
5
  * CORS allowlist middleware for managed mode.
6
6
  *
7
- * Only the single `CP_WEB_ORIGIN` value (provisioner-injected) is permitted.
8
- * All other origins are refused on preflight with a 403.
7
+ * `CP_WEB_ORIGIN` (provisioner-injected) is a comma-separated allowlist of the
8
+ * browser origins the CP UI may be served from — e.g. the Caddy origin and the
9
+ * direct dev port. The request's own origin is echoed back only when it is a
10
+ * member; all other origins are refused on preflight with a 403.
9
11
  */
10
12
  @injectable()
11
13
  export class ManagedCorsMiddleware {
12
- constructor(private readonly allowedOrigin: string) {}
14
+ private readonly allowedOrigins: ReadonlySet<string>;
15
+
16
+ constructor(allowedOrigin: string) {
17
+ this.allowedOrigins = new Set(
18
+ allowedOrigin
19
+ .split(",")
20
+ .map((o) => o.trim())
21
+ .filter(Boolean),
22
+ );
23
+ }
24
+
25
+ private isAllowed(origin: string | undefined): origin is string {
26
+ return origin !== undefined && this.allowedOrigins.has(origin);
27
+ }
13
28
 
14
29
  handle(): MiddlewareHandler {
15
30
  return async (c, next) => {
@@ -17,7 +32,7 @@ export class ManagedCorsMiddleware {
17
32
 
18
33
  // Respond to CORS preflight
19
34
  if (c.req.method === "OPTIONS") {
20
- if (origin === this.allowedOrigin) {
35
+ if (this.isAllowed(origin)) {
21
36
  c.header("access-control-allow-origin", origin);
22
37
  c.header("access-control-allow-methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
23
38
  c.header("access-control-allow-headers", "content-type, authorization");
@@ -29,7 +44,7 @@ export class ManagedCorsMiddleware {
29
44
  }
30
45
 
31
46
  // For actual requests, set CORS headers after the handler runs
32
- if (origin === this.allowedOrigin) {
47
+ if (this.isAllowed(origin)) {
33
48
  await next();
34
49
  c.header("access-control-allow-origin", origin);
35
50
  c.header("access-control-allow-credentials", "true");