@noodleseed/one 0.136.0 → 0.138.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.
Files changed (124) hide show
  1. package/dist/commands/assistant-ops.d.ts.map +1 -1
  2. package/dist/commands/assistant-ops.js +30 -1
  3. package/dist/commands/assistant-ops.js.map +1 -1
  4. package/dist/commands/deploy-first-flow.d.ts +2 -0
  5. package/dist/commands/deploy-first-flow.d.ts.map +1 -1
  6. package/dist/commands/deploy-first-flow.js +46 -14
  7. package/dist/commands/deploy-first-flow.js.map +1 -1
  8. package/dist/commands/deploy-ops.d.ts.map +1 -1
  9. package/dist/commands/deploy-ops.js +27 -1
  10. package/dist/commands/deploy-ops.js.map +1 -1
  11. package/dist/commands/deploy-preflight.d.ts +1 -0
  12. package/dist/commands/deploy-preflight.d.ts.map +1 -1
  13. package/dist/commands/deploy-preflight.js +1 -0
  14. package/dist/commands/deploy-preflight.js.map +1 -1
  15. package/dist/commands/deploy-status-ops.d.ts +1 -0
  16. package/dist/commands/deploy-status-ops.d.ts.map +1 -1
  17. package/dist/commands/deploy-status-ops.js +59 -4
  18. package/dist/commands/deploy-status-ops.js.map +1 -1
  19. package/dist/commands/deployments-ops.d.ts.map +1 -1
  20. package/dist/commands/deployments-ops.js +2 -0
  21. package/dist/commands/deployments-ops.js.map +1 -1
  22. package/dist/commands/diagnostics-ops.d.ts +1 -0
  23. package/dist/commands/diagnostics-ops.d.ts.map +1 -1
  24. package/dist/commands/diagnostics-ops.js +3 -0
  25. package/dist/commands/diagnostics-ops.js.map +1 -1
  26. package/dist/deploy.d.ts +3 -0
  27. package/dist/deploy.d.ts.map +1 -1
  28. package/dist/deploy.js +4 -0
  29. package/dist/deploy.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/node_modules/@noodle-borg/admission-limits/dist/envelope.js +1 -1
  35. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +4 -4
  36. package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +1 -0
  37. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +22 -13
  38. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  39. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.d.ts +6 -2
  40. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.js +5 -2
  41. package/node_modules/@noodle-borg/assistant-gateway/dist/model-request.js +37 -0
  42. package/node_modules/@noodle-borg/assistant-gateway/dist/model-runtime.js +4 -0
  43. package/node_modules/@noodle-borg/{service/dist/assistant-model-stream.js → assistant-gateway/dist/model-stream.js} +34 -2
  44. package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.d.ts +3 -0
  45. package/node_modules/@noodle-borg/assistant-gateway/dist/public-session.js +2 -1
  46. package/node_modules/@noodle-borg/assistant-gateway/dist/public-turn.d.ts +4 -1
  47. package/node_modules/@noodle-borg/assistant-gateway/dist/public-turn.js +2 -1
  48. package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.d.ts +6 -1
  49. package/node_modules/@noodle-borg/assistant-gateway/dist/surface-budget.js +11 -5
  50. package/node_modules/@noodle-borg/assistant-gateway/package.json +5 -1
  51. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +10 -2
  52. package/node_modules/@noodle-borg/authoring/dist/assistant.js +4 -0
  53. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -1
  54. package/node_modules/@noodle-borg/authoring/dist/index.js +1 -1
  55. package/node_modules/@noodle-borg/authoring/dist/load-authored.js +1 -1
  56. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-assistant.js +17 -0
  57. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-hosted-deployment.js +14 -0
  58. package/node_modules/@noodle-borg/compiler/dist/artifact/types.d.ts +1 -7
  59. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +20 -8
  60. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +12 -8
  61. package/node_modules/@noodle-borg/control-plane/dist/portable.d.ts +2 -0
  62. package/node_modules/@noodle-borg/control-plane/dist/portable.js +1 -0
  63. package/node_modules/@noodle-borg/control-plane/dist/rollback-deployment.d.ts +61 -0
  64. package/node_modules/@noodle-borg/{service/dist/operations → control-plane/dist}/rollback-deployment.js +3 -2
  65. package/node_modules/@noodle-borg/control-plane/dist/rollback-result.d.ts +19 -0
  66. package/node_modules/@noodle-borg/control-plane/package.json +3 -1
  67. package/node_modules/@noodle-borg/developer-mcp/dist/contracts.js +4 -0
  68. package/node_modules/@noodle-borg/developer-mcp/dist/widgets/app-overview.js +1 -1
  69. package/node_modules/@noodle-borg/developer-mcp/dist/widgets/deployment-detail.js +1 -1
  70. package/node_modules/@noodle-borg/module/dist/contract.d.ts +1 -0
  71. package/node_modules/@noodle-borg/module/dist/deployment-automation.d.ts +1 -1
  72. package/node_modules/@noodle-borg/module/dist/endpoint-url.d.ts +11 -0
  73. package/node_modules/@noodle-borg/module/dist/endpoint-url.js +20 -0
  74. package/node_modules/@noodle-borg/module/dist/index.d.ts +1 -0
  75. package/node_modules/@noodle-borg/module/dist/index.js +1 -0
  76. package/node_modules/@noodle-borg/observability/dist/assistant-usage.js +217 -0
  77. package/node_modules/@noodle-borg/observability/dist/index.js +2 -0
  78. package/node_modules/@noodle-borg/{service → observability}/dist/request-event-metrics.js +5 -2
  79. package/node_modules/@noodle-borg/observability/dist/request-events-postgres.js +6 -0
  80. package/node_modules/@noodle-borg/observability/dist/request-events.js +2 -0
  81. package/node_modules/@noodle-borg/service/dist/assistant-bindings.js +3 -1
  82. package/node_modules/@noodle-borg/service/dist/developer-mcp/control-plane-adapter.js +3 -3
  83. package/node_modules/@noodle-borg/service/dist/developer-mcp/inspection.js +2 -1
  84. package/node_modules/@noodle-borg/service/dist/registry-access.js +73 -24
  85. package/node_modules/@noodle-borg/service/dist/registry-deploy-transaction.js +5 -2
  86. package/node_modules/@noodle-borg/service/dist/registry-helpers.d.ts +5 -9
  87. package/node_modules/@noodle-borg/service/dist/registry-helpers.js +4 -18
  88. package/node_modules/@noodle-borg/service/dist/registry-rollback.js +3 -1
  89. package/node_modules/@noodle-borg/service/dist/registry-status.js +4 -1
  90. package/node_modules/@noodle-borg/service/dist/registry-targets.js +4 -4
  91. package/node_modules/@noodle-borg/service/dist/registry-types.d.ts +11 -16
  92. package/node_modules/@noodle-borg/service/dist/registry.d.ts +8 -5
  93. package/node_modules/@noodle-borg/service/dist/registry.js +8 -5
  94. package/node_modules/@noodle-borg/service/dist/routes/access.js +19 -1
  95. package/node_modules/@noodle-borg/service/dist/routes/analytics-dispatch.js +17 -1
  96. package/node_modules/@noodle-borg/service/dist/routes/analytics.js +1 -1
  97. package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +73 -59
  98. package/node_modules/@noodle-borg/service/dist/routes/assistant-doctor.js +1 -1
  99. package/node_modules/@noodle-borg/service/dist/routes/assistant-model-binding.js +28 -0
  100. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +18 -0
  101. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-turn.js +6 -0
  102. package/node_modules/@noodle-borg/service/dist/routes/assistant-usage.js +42 -0
  103. package/node_modules/@noodle-borg/service/dist/routes/assistant.js +20 -3
  104. package/node_modules/@noodle-borg/service/dist/routes/auth-doctor-live.js +1 -2
  105. package/node_modules/@noodle-borg/service/dist/routes/control-plane.js +49 -4
  106. package/node_modules/@noodle-borg/service/dist/routes/deploy-preflight.js +11 -1
  107. package/node_modules/@noodle-borg/service/dist/routes/endpoint-enrichment.js +2 -1
  108. package/node_modules/@noodle-borg/service/dist/routes/paths.js +4 -0
  109. package/node_modules/@noodle-borg/service/dist/routes/rollback.js +1 -1
  110. package/node_modules/@noodle-borg/service/dist/service.js +6 -0
  111. package/node_modules/@noodle-borg/service/dist/store/in-memory.d.ts +2 -5
  112. package/node_modules/@noodle-borg/service/dist/store/in-memory.js +9 -21
  113. package/node_modules/@noodle-borg/service/dist/store/json-file.d.ts +2 -5
  114. package/node_modules/@noodle-borg/service/dist/store/json-file.js +8 -20
  115. package/node_modules/@noodle-borg/service/dist/store/records.js +39 -0
  116. package/node_modules/@noodle-borg/service/dist/store.d.ts +21 -10
  117. package/node_modules/@noodle-borg/service/package.json +5 -1
  118. package/node_modules/@noodle-borg/wire-contracts/dist/control-plane.d.ts +120 -0
  119. package/node_modules/@noodle-borg/wire-contracts/dist/control-plane.js +123 -1
  120. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +4 -0
  121. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +15 -1
  122. package/node_modules/@noodleseed/assistant/README.md +43 -8
  123. package/node_modules/@noodleseed/assistant/package.json +16 -2
  124. package/package.json +2 -2
@@ -10,8 +10,11 @@ function hourBucket(iso) {
10
10
  return `${iso.slice(0, 13)}:00:00.000Z`;
11
11
  }
12
12
  export function aggregateRequestEvents(events) {
13
- const usage = events.filter((e) => e.kind === 'usage');
14
- const discovery = events.length - usage.length;
13
+ // Assistant lifecycle records share the durable tenant stream but have their own aggregate. Keep
14
+ // the long-standing MCP metrics contract stable rather than counting one user turn as an MCP call.
15
+ const mcpEvents = events.filter((event) => event.method !== 'assistant');
16
+ const usage = mcpEvents.filter((event) => event.kind === 'usage');
17
+ const discovery = mcpEvents.length - usage.length;
15
18
  const durations = usage.map((e) => e.durationMs).sort((a, b) => a - b);
16
19
  let ok = 0;
17
20
  let toolErrors = 0;
@@ -51,6 +51,10 @@ export async function ensureRequestEventSchema(pool) {
51
51
  await pool.query(`
52
52
  CREATE INDEX IF NOT EXISTS request_events_tenant_time_idx
53
53
  ON request_events (org_slug, app_slug, environment, seq DESC)
54
+ `);
55
+ await pool.query(`
56
+ CREATE INDEX IF NOT EXISTS request_events_tenant_method_time_idx
57
+ ON request_events (org_slug, app_slug, environment, method, seq DESC)
54
58
  `);
55
59
  await pool.query(`
56
60
  CREATE INDEX IF NOT EXISTS request_events_session_idx
@@ -128,6 +132,8 @@ export class PostgresRequestEventStore {
128
132
  add('app_slug', filter.app);
129
133
  if (filter.env !== undefined)
130
134
  add('environment', filter.env);
135
+ if (filter.method !== undefined)
136
+ add('method', filter.method);
131
137
  if (filter.outcome !== undefined)
132
138
  add('outcome', filter.outcome);
133
139
  if (filter.toolName !== undefined)
@@ -41,6 +41,8 @@ function matches(event, filter) {
41
41
  return false;
42
42
  if (filter.env !== undefined && event.env !== filter.env)
43
43
  return false;
44
+ if (filter.method !== undefined && event.method !== filter.method)
45
+ return false;
44
46
  if (filter.outcome !== undefined && event.outcome !== filter.outcome)
45
47
  return false;
46
48
  if (filter.toolName !== undefined && event.toolName !== filter.toolName)
@@ -3,7 +3,7 @@ import { missingSecretErrors, missingVariableErrors } from './registry-helpers.j
3
3
  import { serverAuthSecretBindings } from './server-auth-bindings.js';
4
4
  const VARIABLE = /\$\{env\.([A-Za-z0-9_]+)\}/g;
5
5
  export function assistantSecretBindings(assistant) {
6
- return assistant
6
+ return assistant?.model.kind === 'openai-compatible'
7
7
  ? [
8
8
  {
9
9
  connectorId: 'server.assistant',
@@ -17,6 +17,8 @@ export function assistantSecretBindings(assistant) {
17
17
  export function assistantVariableBindings(assistant) {
18
18
  if (!assistant)
19
19
  return [];
20
+ if (assistant.model.kind !== 'openai-compatible')
21
+ return [];
20
22
  const names = new Set();
21
23
  for (const value of [assistant.model.baseUrl, assistant.model.model]) {
22
24
  for (const match of value.matchAll(VARIABLE)) {
@@ -1,7 +1,6 @@
1
- import { mcpSubdomainEndpointOptions } from '@noodle-borg/control-plane/portable';
1
+ import { mcpSubdomainEndpointOptions, rollbackDeploymentOperation, } from '@noodle-borg/control-plane/portable';
2
2
  import { DeveloperControlPlaneError, } from '@noodle-borg/developer-mcp';
3
- import { rollbackDeploymentOperation } from '../operations/rollback-deployment.js';
4
- import { aggregateRequestEvents } from '../request-event-metrics.js';
3
+ import { aggregateRequestEvents } from '@noodle-borg/observability';
5
4
  import { inspectCompiledServiceDeployment, inspectServiceDeployment } from './inspection.js';
6
5
  const METRICS_SCAN_LIMIT = 50_000;
7
6
  const WINDOWS_MS = {
@@ -261,6 +260,7 @@ function mapDeployment(deployment) {
261
260
  serverName: deployment.serverName,
262
261
  createdAt: deployment.createdAt,
263
262
  accessMode: deployment.accessMode,
263
+ ...(deployment.ownerSubject !== undefined ? { ownerSubject: deployment.ownerSubject } : {}),
264
264
  ...(deployment.endpointUrl !== undefined ? { endpointUrl: deployment.endpointUrl } : {}),
265
265
  };
266
266
  }
@@ -1,4 +1,4 @@
1
- import { tenantMcpUrl } from '../registry-helpers.js';
1
+ import { tenantMcpUrl } from '@noodle-borg/module';
2
2
  export async function inspectServiceDeployment(registry, tenant, baseUrl, endpointOptions = {}) {
3
3
  const status = await registry.getStatus(tenant, baseUrl, undefined, endpointOptions);
4
4
  if (status === undefined)
@@ -109,6 +109,7 @@ export function inspectCompiledServiceDeployment(deployment, artifact, baseUrl,
109
109
  ? {}
110
110
  : { createdByEmail: deployment.createdByEmail }),
111
111
  accessMode: deployment.accessMode,
112
+ ...(deployment.ownerSubject !== undefined ? { ownerSubject: deployment.ownerSubject } : {}),
112
113
  },
113
114
  health: { state: 'ready', missingSecrets: [] },
114
115
  surface,
@@ -1,12 +1,13 @@
1
1
  import { assertUniqueActiveCustomerAuthAudienceBindings, CustomerAuthAudienceConflictError, } from './customer-auth-audience-binding.js';
2
+ import { deploymentOwnerSubject } from './registry-helpers.js';
2
3
  import { activeRecord, activeRecordVersion } from './registry-state.js';
3
4
  import { tenantDeploymentKey, tryServedTargetFor } from './registry-targets.js';
4
5
  /** Registry-facing access transition with customer audience ownership preflight and atomic translation. */
5
- export async function updateRegistryAccess(state, tenant, nextAccessMode, serverVersion, loadServed, customerVerifierFactory, hasCustomerAuthConflict) {
6
- if (nextAccessMode === 'customers') {
7
- const record = serverVersion === undefined
6
+ export async function updateRegistryAccess(state, tenant, options, loadServed, customerVerifierFactory, hasCustomerAuthConflict) {
7
+ if (options.accessMode === 'customers') {
8
+ const record = options.serverVersion === undefined
8
9
  ? await activeRecord(state, tenant)
9
- : await activeRecordVersion(state, tenant, serverVersion);
10
+ : await activeRecordVersion(state, tenant, options.serverVersion);
10
11
  const served = record === undefined ? undefined : await loadServed(record.deploymentId);
11
12
  const auth = served?.served.artifact.server.auth;
12
13
  if (record !== undefined &&
@@ -16,7 +17,7 @@ export async function updateRegistryAccess(state, tenant, nextAccessMode, server
16
17
  }
17
18
  }
18
19
  try {
19
- return await updateActiveAccess(state, tenant, nextAccessMode, serverVersion, loadServed, customerVerifierFactory);
20
+ return await updateActiveAccess(state, tenant, options, loadServed, customerVerifierFactory);
20
21
  }
21
22
  catch (error) {
22
23
  if (error instanceof CustomerAuthAudienceConflictError) {
@@ -34,14 +35,19 @@ export function manifestUsesUserRoot(manifest) {
34
35
  * access mode supplies the optimistic guard; in-memory state is reconciled only after the store accepts
35
36
  * the compare-and-set.
36
37
  */
37
- export async function updateActiveAccess(state, tenant, nextAccessMode, serverVersion, loadServed, customerVerifierFactory) {
38
- const record = serverVersion === undefined
38
+ export async function updateActiveAccess(state, tenant, options, loadServed, customerVerifierFactory) {
39
+ const record = options.serverVersion === undefined
39
40
  ? await activeRecord(state, tenant)
40
- : await activeRecordVersion(state, tenant, serverVersion);
41
+ : await activeRecordVersion(state, tenant, options.serverVersion);
41
42
  if (record === undefined)
42
43
  return noActiveDeployment();
44
+ const nextAccessMode = options.accessMode;
43
45
  const previousAccessMode = record.accessMode ?? 'owner-only';
44
- const served = previousAccessMode !== nextAccessMode || nextAccessMode === 'customers'
46
+ const previousOwnerSubject = deploymentOwnerSubject(record);
47
+ const nextOwnerSubject = options.ownerSubject ?? previousOwnerSubject;
48
+ const accessChanged = previousAccessMode !== nextAccessMode;
49
+ const ownerChanged = previousOwnerSubject !== nextOwnerSubject;
50
+ const served = accessChanged || ownerChanged || nextAccessMode === 'customers'
45
51
  ? await loadServed(record.deploymentId)
46
52
  : undefined;
47
53
  const serverAuth = served?.served.artifact.server.auth;
@@ -53,20 +59,33 @@ export async function updateActiveAccess(state, tenant, nextAccessMode, serverVe
53
59
  message: 'Customer access requires server authentication.',
54
60
  };
55
61
  }
56
- if (previousAccessMode === nextAccessMode) {
62
+ if (!accessChanged && !ownerChanged) {
57
63
  const committed = state.store === undefined
58
- ? commitInMemoryAccessUpdate(state, tenant, record, nextAccessMode, serverAuth)
59
- : await state.store.updateActiveAccess(tenant, record.deploymentId, nextAccessMode, {
64
+ ? commitInMemoryAccessUpdate(state, tenant, record, {
65
+ accessMode: nextAccessMode,
60
66
  expectedAccessMode: record.accessMode,
67
+ expectedOwnerSubject: previousOwnerSubject,
68
+ ...(serverAuth !== undefined ? { serverAuth } : {}),
69
+ })
70
+ : await state.store.updateActiveAccess(tenant, record.deploymentId, {
71
+ accessMode: nextAccessMode,
72
+ expectedAccessMode: record.accessMode,
73
+ expectedOwnerSubject: previousOwnerSubject,
61
74
  ...(serverAuth !== undefined ? { serverAuth } : {}),
62
75
  });
63
76
  if (committed === undefined)
64
77
  return accessUpdateConflict();
65
78
  reconcileCommittedAccess(state, tenant, committed);
66
- return { ok: true, changed: false, previousAccessMode, record: committed };
79
+ return {
80
+ ok: true,
81
+ changed: false,
82
+ accessChanged: false,
83
+ ownerChanged: false,
84
+ previousAccessMode,
85
+ ...(previousOwnerSubject !== undefined ? { previousOwnerSubject } : {}),
86
+ record: committed,
87
+ };
67
88
  }
68
- if (served === undefined)
69
- return accessUpdateConflict();
70
89
  if (nextAccessMode === 'public' && manifestUsesUserRoot(record.manifest)) {
71
90
  return {
72
91
  ok: false,
@@ -75,7 +94,7 @@ export async function updateActiveAccess(state, tenant, nextAccessMode, serverVe
75
94
  message: 'Public access cannot reference the user context.',
76
95
  };
77
96
  }
78
- if (nextAccessMode === 'owner-only' && record.createdBySubject === undefined) {
97
+ if (nextAccessMode === 'owner-only' && nextOwnerSubject === undefined) {
79
98
  return {
80
99
  ok: false,
81
100
  status: 409,
@@ -83,27 +102,51 @@ export async function updateActiveAccess(state, tenant, nextAccessMode, serverVe
83
102
  message: 'Owner-only access requires a recorded deployer identity.',
84
103
  };
85
104
  }
105
+ if (served === undefined)
106
+ return accessUpdateConflict();
86
107
  const updated = {
87
108
  ...record,
88
109
  accessMode: nextAccessMode,
110
+ ...(ownerChanged && nextOwnerSubject !== undefined ? { ownerSubject: nextOwnerSubject } : {}),
89
111
  ...(nextAccessMode === 'customers' && serverAuth !== undefined ? { serverAuth } : {}),
90
112
  };
91
113
  const target = tryServedTargetFor(updated, served.served, customerVerifierFactory);
92
114
  if (target === undefined)
93
115
  return accessUpdateConflict();
94
116
  const committed = state.store === undefined
95
- ? commitInMemoryAccessUpdate(state, tenant, record, nextAccessMode, serverAuth)
96
- : await state.store.updateActiveAccess(tenant, record.deploymentId, nextAccessMode, {
117
+ ? commitInMemoryAccessUpdate(state, tenant, record, {
118
+ accessMode: nextAccessMode,
119
+ ...(ownerChanged && nextOwnerSubject !== undefined
120
+ ? { ownerSubject: nextOwnerSubject }
121
+ : {}),
122
+ expectedAccessMode: record.accessMode,
123
+ expectedOwnerSubject: previousOwnerSubject,
124
+ ...(serverAuth !== undefined ? { serverAuth } : {}),
125
+ })
126
+ : await state.store.updateActiveAccess(tenant, record.deploymentId, {
127
+ accessMode: nextAccessMode,
128
+ ...(ownerChanged && nextOwnerSubject !== undefined
129
+ ? { ownerSubject: nextOwnerSubject }
130
+ : {}),
97
131
  expectedAccessMode: record.accessMode,
132
+ expectedOwnerSubject: previousOwnerSubject,
98
133
  ...(serverAuth !== undefined ? { serverAuth } : {}),
99
134
  });
100
135
  if (committed === undefined)
101
136
  return accessUpdateConflict();
102
137
  reconcileCommittedAccess(state, tenant, committed);
103
138
  state.servers.set(committed.deploymentId, target);
104
- return { ok: true, changed: true, previousAccessMode, record: committed };
139
+ return {
140
+ ok: true,
141
+ changed: true,
142
+ accessChanged,
143
+ ownerChanged,
144
+ previousAccessMode,
145
+ ...(previousOwnerSubject !== undefined ? { previousOwnerSubject } : {}),
146
+ record: committed,
147
+ };
105
148
  }
106
- function commitInMemoryAccessUpdate(state, tenant, observed, nextAccessMode, serverAuth) {
149
+ function commitInMemoryAccessUpdate(state, tenant, observed, input) {
107
150
  const current = state.records.get(observed.deploymentId);
108
151
  if (current === undefined ||
109
152
  !current.active ||
@@ -111,16 +154,22 @@ function commitInMemoryAccessUpdate(state, tenant, observed, nextAccessMode, ser
111
154
  current.orgSlug !== tenant.org ||
112
155
  current.appSlug !== tenant.app ||
113
156
  current.environment !== tenant.env ||
114
- current.accessMode !== observed.accessMode) {
157
+ current.accessMode !== input.expectedAccessMode ||
158
+ deploymentOwnerSubject(current) !== input.expectedOwnerSubject) {
115
159
  return undefined;
116
160
  }
117
- if ((current.accessMode ?? 'owner-only') === nextAccessMode && nextAccessMode !== 'customers') {
161
+ if ((current.accessMode ?? 'owner-only') === input.accessMode &&
162
+ (input.ownerSubject === undefined || deploymentOwnerSubject(current) === input.ownerSubject) &&
163
+ input.accessMode !== 'customers') {
118
164
  return current;
119
165
  }
120
166
  const committed = {
121
167
  ...current,
122
- accessMode: nextAccessMode,
123
- ...(nextAccessMode === 'customers' && serverAuth !== undefined ? { serverAuth } : {}),
168
+ accessMode: input.accessMode,
169
+ ...(input.ownerSubject !== undefined ? { ownerSubject: input.ownerSubject } : {}),
170
+ ...(input.accessMode === 'customers' && input.serverAuth !== undefined
171
+ ? { serverAuth: input.serverAuth }
172
+ : {}),
124
173
  };
125
174
  const proposed = new Map(state.records).set(committed.deploymentId, committed);
126
175
  assertUniqueActiveCustomerAuthAudienceBindings([...proposed.values()]);
@@ -1,5 +1,5 @@
1
1
  import { normalizeServerVersion } from '@noodle-borg/module';
2
- import { deployAccessRequiresActor, deployerRequiredError, emptyMembershipSourcesError, membershipSourcesRequireOrgMembersError, missingCapabilityErrors, serverAuthRequiredError, } from './registry-helpers.js';
2
+ import { deployAccessRequiresActor, deployerRequiredError, deploymentOwnerSubject, emptyMembershipSourcesError, membershipSourcesRequireOrgMembersError, missingCapabilityErrors, serverAuthRequiredError, } from './registry-helpers.js';
3
3
  import { idempotentDeploymentId, mintDeploymentId } from './registry-targets.js';
4
4
  export async function preflightRegistryDeploy(input) {
5
5
  const { actor, accessMode, orgMembershipSources } = input.options;
@@ -56,6 +56,7 @@ export async function resolveDeployAttempt(input) {
56
56
  },
57
57
  };
58
58
  }
59
+ const ownerSubject = deploymentOwnerSubject(existing);
59
60
  return {
60
61
  deploymentId,
61
62
  replay: {
@@ -64,6 +65,7 @@ export async function resolveDeployAttempt(input) {
64
65
  deploymentVersion: existing.deploymentVersion,
65
66
  ...(existing.serverVersion !== undefined ? { serverVersion: existing.serverVersion } : {}),
66
67
  ...(existing.accessMode !== undefined ? { accessMode: existing.accessMode } : {}),
68
+ ...(ownerSubject !== undefined ? { ownerSubject } : {}),
67
69
  replayed: true,
68
70
  },
69
71
  };
@@ -82,7 +84,8 @@ function sameIdempotentDeploy(record, tenant, manifest, options) {
82
84
  record.deploymentSource === options.deploymentSource &&
83
85
  sameJson(record.hostedAssets, options.hostedAssets) &&
84
86
  sameJson(record.orgMembershipSources, options.orgMembershipSources) &&
85
- (options.accessMode !== 'owner-only' || record.createdBySubject === options.actor?.subject));
87
+ (options.accessMode !== 'owner-only' ||
88
+ deploymentOwnerSubject(record) === (options.ownerSubject ?? options.actor?.subject)));
86
89
  }
87
90
  function sameJson(left, right) {
88
91
  return JSON.stringify(left) === JSON.stringify(right);
@@ -3,26 +3,22 @@ import type { SecretBinding } from '@noodle-borg/connector-defs';
3
3
  import type { OrgMembershipSource } from '@noodle-borg/module';
4
4
  import type { AccessMode } from '@noodle-borg/transport-http';
5
5
  import type { DeployError } from './registry.js';
6
- import type { TenantRef } from './store.js';
6
+ import type { DeployRecord } from './store.js';
7
7
  /**
8
8
  * Provenance of an env's current active deployment — the inherit source for GHD-3 run-deploy
9
- * access-mode resolution (ADR 0132 Decision 4). The owner subject/email let an inherited `owner-only`
10
- * run deploy preserve the PREVIOUS human owner instead of rebinding the app to the automation subject.
9
+ * access-mode resolution (ADR 0132 Decision 4). The owner subject lets an inherited `owner-only` run deploy
10
+ * preserve the PREVIOUS human owner instead of rebinding the app to the automation subject.
11
11
  */
12
12
  export type ActiveDeployProvenance = {
13
13
  readonly accessMode: AccessMode | undefined;
14
14
  readonly orgMembershipSources: readonly OrgMembershipSource[] | undefined;
15
15
  readonly ownerSubject: string | undefined;
16
- readonly ownerEmail: string | undefined;
17
16
  } | undefined;
17
+ /** Resolve the durable owner binding while keeping legacy creator-only records readable. */
18
+ export declare function deploymentOwnerSubject(record: DeployRecord): string | undefined;
18
19
  export declare function missingCapabilityErrors(required: readonly CapabilityName[], available: readonly CapabilityName[]): readonly DeployError[];
19
20
  export declare function missingSecretErrors(bindings: readonly SecretBinding[], secrets: Readonly<Record<string, string>>): DeployError[];
20
21
  export declare function missingVariableErrors(bindings: readonly string[], variables: Readonly<Record<string, string>>): DeployError[];
21
- export interface EndpointUrlOptions {
22
- readonly publicBaseDomain?: string;
23
- readonly mcpSubdomain?: string;
24
- }
25
- export declare function tenantMcpUrl(base: string, tenant: TenantRef, serverVersion?: string, options?: EndpointUrlOptions): string;
26
22
  export declare function deployAccessRequiresActor(accessMode: AccessMode): boolean;
27
23
  /** An identity access mode was requested without an authenticated deployer. */
28
24
  export declare function deployerRequiredError(accessMode: AccessMode): {
@@ -1,4 +1,7 @@
1
- import { formatPublicMcpUrl, serverVersionPathSegment } from '@noodle-borg/module';
1
+ /** Resolve the durable owner binding while keeping legacy creator-only records readable. */
2
+ export function deploymentOwnerSubject(record) {
3
+ return record.ownerSubject ?? record.createdBySubject;
4
+ }
2
5
  export function missingCapabilityErrors(required, available) {
3
6
  const enabled = new Set(available);
4
7
  return required
@@ -41,23 +44,6 @@ export function missingVariableErrors(bindings, variables) {
41
44
  }
42
45
  return errors;
43
46
  }
44
- export function tenantMcpUrl(base, tenant, serverVersion, options = {}) {
45
- if (options.publicBaseDomain !== undefined) {
46
- if (options.mcpSubdomain === undefined) {
47
- throw new Error('MCP subdomain is required to format a public endpoint URL');
48
- }
49
- return formatPublicMcpUrl(options.publicBaseDomain, {
50
- mcpSubdomain: options.mcpSubdomain,
51
- app: tenant.app,
52
- env: tenant.env,
53
- ...(serverVersion !== undefined ? { serverVersion } : {}),
54
- });
55
- }
56
- const versionPath = serverVersion !== undefined ? `/${serverVersionPathSegment(serverVersion)}` : '';
57
- return tenant.env === 'prod'
58
- ? `${base}/o/${tenant.org}/${tenant.app}${versionPath}/mcp`
59
- : `${base}/o/${tenant.org}/${tenant.app}/${tenant.env}${versionPath}/mcp`;
60
- }
61
47
  export function deployAccessRequiresActor(accessMode) {
62
48
  return accessMode !== 'public' && accessMode !== 'mixed';
63
49
  }
@@ -1,4 +1,4 @@
1
- import { missingCapabilityErrors } from './registry-helpers.js';
1
+ import { deploymentOwnerSubject, missingCapabilityErrors } from './registry-helpers.js';
2
2
  import { activateInMemory } from './registry-state.js';
3
3
  import { recordMatchesTenant, servedTargetFor, tenantDeploymentKey, tenantKey, tryServedTargetFor, } from './registry-targets.js';
4
4
  /**
@@ -101,6 +101,7 @@ export async function rollbackDeployment(state, ref, deploymentId, compilePersis
101
101
  state.servers.set(active.deploymentId, servedTargetFor(active, built.served, customerVerifierFactory));
102
102
  state.activeTenants.set(tenantDeploymentKey(ref, active.serverVersion), active.deploymentId);
103
103
  state.activeTenants.delete(tenantKey(ref));
104
+ const ownerSubject = deploymentOwnerSubject(active);
104
105
  return {
105
106
  ok: true,
106
107
  deploymentId: active.deploymentId,
@@ -108,6 +109,7 @@ export async function rollbackDeployment(state, ref, deploymentId, compilePersis
108
109
  ...(previous !== undefined ? { previousDeploymentId: previous.deploymentId } : {}),
109
110
  alreadyActive: activation.alreadyActive,
110
111
  accessMode: active.accessMode ?? 'owner-only',
112
+ ...(ownerSubject !== undefined ? { ownerSubject } : {}),
111
113
  ...(previous?.accessMode !== undefined ? { previousAccessMode: previous.accessMode } : {}),
112
114
  serverName: active.serverName,
113
115
  createdAt: active.createdAt,
@@ -1,4 +1,5 @@
1
- import { missingSecretNames, tenantMcpUrl } from './registry-helpers.js';
1
+ import { tenantMcpUrl } from '@noodle-borg/module';
2
+ import { deploymentOwnerSubject, missingSecretNames } from './registry-helpers.js';
2
3
  /** Project one compiled deployment into the control-plane status contract. */
3
4
  export function deploymentStatusFor(ref, record, built, baseUrl, endpointOptions) {
4
5
  const missingSecrets = built.ok ? [] : missingSecretNames(built.errors);
@@ -6,6 +7,7 @@ export function deploymentStatusFor(ref, record, built, baseUrl, endpointOptions
6
7
  record.accessMode === 'customers' &&
7
8
  built.served.artifact.server.auth === undefined;
8
9
  const unhealthy = missingCustomerAuth || (!built.ok && missingSecrets.length === 0);
10
+ const ownerSubject = deploymentOwnerSubject(record);
9
11
  return {
10
12
  target: ref,
11
13
  deployment: {
@@ -16,6 +18,7 @@ export function deploymentStatusFor(ref, record, built, baseUrl, endpointOptions
16
18
  serverName: record.serverName,
17
19
  createdAt: record.createdAt,
18
20
  ...(record.createdByEmail !== undefined ? { createdByEmail: record.createdByEmail } : {}),
21
+ ...(ownerSubject !== undefined ? { ownerSubject } : {}),
19
22
  accessMode: record.accessMode ?? 'owner-only',
20
23
  ...(record.deploymentLock !== undefined
21
24
  ? {
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { parseAppPackageSnapshot } from './app-package-snapshot.js';
3
3
  import { hasExactCustomerAuthProjection } from './customer-auth-audience-binding.js';
4
+ import { deploymentOwnerSubject } from './registry-helpers.js';
4
5
  /**
5
6
  * Build the front-door {@link ServedTarget} for a persisted record: its access mode plus the credential the
6
7
  * front-door checks. Legacy alpha records without an identity access mode are not served.
@@ -18,6 +19,7 @@ export function servedTargetFor(record, served, customerVerifierFactory) {
18
19
  throw new MissingCustomerAuthError(record.deploymentId);
19
20
  }
20
21
  const boundServed = bindAppPackageSnapshot(served, record.appPackageSnapshot);
22
+ const ownerSubject = deploymentOwnerSubject(record);
21
23
  return {
22
24
  served: boundServed,
23
25
  deploymentId: record.deploymentId,
@@ -28,9 +30,7 @@ export function servedTargetFor(record, served, customerVerifierFactory) {
28
30
  ...(accessMode === 'org-members' && record.orgMembershipSources !== undefined
29
31
  ? { orgMembershipSources: record.orgMembershipSources }
30
32
  : {}),
31
- ...(accessMode === 'owner-only' && record.createdBySubject !== undefined
32
- ? { ownerSubject: record.createdBySubject }
33
- : {}),
33
+ ...(accessMode === 'owner-only' && ownerSubject !== undefined ? { ownerSubject } : {}),
34
34
  ...(customerAuth !== undefined
35
35
  ? customerTargetFields(customerAuth, customerVerifierFactory)
36
36
  : {}),
@@ -55,7 +55,7 @@ export function tryServedTargetFor(record, served, customerVerifierFactory) {
55
55
  }
56
56
  }
57
57
  export function servedTargetMatchesRecord(target, record) {
58
- const expectedOwner = record.accessMode === 'owner-only' ? record.createdBySubject : undefined;
58
+ const expectedOwner = record.accessMode === 'owner-only' ? deploymentOwnerSubject(record) : undefined;
59
59
  const expectedMembership = record.accessMode === 'org-members' ? record.orgMembershipSources : undefined;
60
60
  return (target.deploymentId === record.deploymentId &&
61
61
  target.accessMode === record.accessMode &&
@@ -22,6 +22,8 @@ export interface DeployOptions {
22
22
  readonly connectors?: string | undefined;
23
23
  readonly actor?: ControlPlaneIdentity | undefined;
24
24
  readonly accessMode?: AccessMode | undefined;
25
+ /** Effective OAuth subject for an owner-only deployment, distinct from the deploy actor. */
26
+ readonly ownerSubject?: string | undefined;
25
27
  readonly orgMembershipSources?: readonly OrgMembershipSource[] | undefined;
26
28
  readonly hostedAssets?: readonly HostedPackagedAsset[] | undefined;
27
29
  readonly serverVersion?: string | undefined;
@@ -46,6 +48,7 @@ export type DeployResult = {
46
48
  readonly deploymentVersion: number;
47
49
  readonly serverVersion?: string;
48
50
  readonly accessMode?: AccessMode;
51
+ readonly ownerSubject?: string;
49
52
  readonly replayed?: boolean;
50
53
  } | {
51
54
  readonly ok: false;
@@ -69,27 +72,14 @@ export type RunDeployResult = DeployResult | {
69
72
  readonly deploymentVersion: number;
70
73
  readonly serverVersion?: string;
71
74
  };
72
- export type RollbackResult = {
73
- readonly ok: true;
74
- readonly deploymentId: string;
75
- readonly serverVersion?: string;
76
- readonly previousDeploymentId?: string;
77
- readonly alreadyActive: boolean;
78
- readonly accessMode: AccessMode;
79
- readonly previousAccessMode?: AccessMode;
80
- readonly serverName: string;
81
- readonly createdAt: string;
82
- } | {
83
- readonly ok: false;
84
- readonly status: 404 | 409;
85
- readonly code?: 'deployment_locked';
86
- readonly error: string;
87
- };
88
75
  export type AccessUpdateFailureCode = 'no_active_deployment' | 'server_auth_required' | 'customer_auth_audience_conflict' | 'public_user_context_conflict' | 'owner_identity_required' | 'access_update_conflict';
89
76
  export type AccessUpdateResult = {
90
77
  readonly ok: true;
91
78
  readonly changed: boolean;
79
+ readonly accessChanged: boolean;
80
+ readonly ownerChanged: boolean;
92
81
  readonly previousAccessMode: AccessMode;
82
+ readonly previousOwnerSubject?: string;
93
83
  readonly record: DeployRecord;
94
84
  } | {
95
85
  readonly ok: false;
@@ -97,6 +87,11 @@ export type AccessUpdateResult = {
97
87
  readonly code: AccessUpdateFailureCode;
98
88
  readonly message: string;
99
89
  };
90
+ export interface AccessUpdateOptions {
91
+ readonly accessMode: AccessMode;
92
+ readonly ownerSubject?: string | undefined;
93
+ readonly serverVersion?: string | undefined;
94
+ }
100
95
  export interface ServerRegistryOptions {
101
96
  /** Pure renderer injection used for deployment-bound package construction and failure testing. */
102
97
  readonly renderAppPackage?: AppPackageRenderer;
@@ -1,14 +1,17 @@
1
1
  import type { CapabilityName } from '@noodle-borg/capabilities';
2
2
  import type { CatalogConnector, LocalAssetOptions, PackagedAsset } from '@noodle-borg/compiler';
3
+ import type { RollbackResult } from '@noodle-borg/control-plane/portable';
3
4
  import { type KnowledgeDeployHooks, type KnowledgeSearchPortFactory } from '@noodle-borg/knowledge-operations/portable';
4
5
  import type { PlatformIdentityRecoveryPlan } from '@noodle-borg/module';
6
+ import { type EndpointUrlOptions } from '@noodle-borg/module';
5
7
  import type { Connector, SecretBox } from '@noodle-borg/runtime';
6
- import type { AccessMode, ServedTarget } from '@noodle-borg/transport-http';
7
- import { type ActiveDeployProvenance, type EndpointUrlOptions } from './registry-helpers.js';
8
- import type { AccessUpdateResult, DeployOptions, DeployPreflightResult, RecoverResult, RollbackResult, RunDeployResult, ServerRegistryOptions } from './registry-types.js';
8
+ import type { ServedTarget } from '@noodle-borg/transport-http';
9
+ import { type ActiveDeployProvenance } from './registry-helpers.js';
10
+ import type { AccessUpdateOptions, AccessUpdateResult, DeployOptions, DeployPreflightResult, RecoverResult, RunDeployResult, ServerRegistryOptions } from './registry-types.js';
9
11
  import { type AppArchiveResult, type AppRestoreResult, type AppSummary, type ArtifactStore, type ConfigStore, type CustomerAuthRestoreProjection, type DeploymentListFilter, type DeploymentLock, type DeploymentLockUpdateResult, type DeploymentStatus, type DeploymentSummary, type DeployRecord, type EnvSummary, type ProductionEnvironmentChange, type TenantRef } from './store.js';
12
+ export type { RollbackResult } from '@noodle-borg/control-plane/portable';
10
13
  export type { DeploymentPackage } from './registry-package.js';
11
- export type { AccessUpdateFailureCode, AccessUpdateResult, DeployError, DeployOptions, DeployPreflightResult, DeployResult, RecoverResult, RollbackResult, ServerRegistryOptions, } from './registry-types.js';
14
+ export type { AccessUpdateFailureCode, AccessUpdateOptions, AccessUpdateResult, DeployError, DeployOptions, DeployPreflightResult, DeployResult, RecoverResult, ServerRegistryOptions, } from './registry-types.js';
12
15
  export declare class ServerRegistry {
13
16
  #private;
14
17
  constructor(store?: ArtifactStore, _secretBox?: SecretBox, configStore?: ConfigStore, options?: ServerRegistryOptions);
@@ -54,7 +57,7 @@ export declare class ServerRegistry {
54
57
  reconcilePlatformAccountReset(input: PlatformIdentityRecoveryPlan): void;
55
58
  customerAuthRestoreProjections(deploymentIds: readonly string[]): Promise<readonly CustomerAuthRestoreProjection[]>;
56
59
  getStatus(ref: TenantRef, baseUrl: string, serverVersion?: string, endpointOptions?: EndpointUrlOptions): Promise<DeploymentStatus | undefined>;
57
- updateAccess(ref: TenantRef, accessMode: AccessMode, serverVersion?: string): Promise<AccessUpdateResult>;
60
+ updateAccess(ref: TenantRef, options: AccessUpdateOptions): Promise<AccessUpdateResult>;
58
61
  setDeploymentLock(ref: TenantRef, serverVersion: string, expectedDeploymentId: string, deploymentLock: DeploymentLock | undefined): Promise<DeploymentLockUpdateResult>;
59
62
  rollback(ref: TenantRef, deploymentId: string): Promise<RollbackResult>;
60
63
  get configStore(): ConfigStore;
@@ -7,7 +7,7 @@ import { defaultActiveRecord } from './deployment-versioning.js';
7
7
  import { updateRegistryAccess } from './registry-access.js';
8
8
  import { compileRegistryTarget } from './registry-compile.js';
9
9
  import { preflightRegistryDeploy, resolveDeployAttempt } from './registry-deploy-transaction.js';
10
- import { missingCapabilityErrors, } from './registry-helpers.js';
10
+ import { deploymentOwnerSubject, missingCapabilityErrors, } from './registry-helpers.js';
11
11
  import { registryDeploymentPackage, renderDeploymentPackageSnapshot } from './registry-package.js';
12
12
  import { recoverRegistryRecords } from './registry-recover.js';
13
13
  import { rollbackDeployment, rollbackWithMappedErrors } from './registry-rollback.js';
@@ -130,6 +130,7 @@ export class ServerRegistry {
130
130
  const version = Date.now();
131
131
  const built = preflight.bindDeployment(deploymentId);
132
132
  const serverAuth = built.artifact.server.auth;
133
+ const ownerSubject = accessMode === 'owner-only' ? (options.ownerSubject ?? actor?.subject) : undefined;
133
134
  const record = {
134
135
  schemaVersion: 1,
135
136
  deploymentId,
@@ -142,6 +143,7 @@ export class ServerRegistry {
142
143
  serverName: built.artifact.server.name,
143
144
  createdAt: new Date().toISOString(),
144
145
  ...(actor ? { createdBySubject: actor.subject, createdByEmail: actor.email } : {}),
146
+ ...(ownerSubject !== undefined ? { ownerSubject } : {}),
145
147
  ...(deploymentSource !== undefined ? { deploymentSource } : {}),
146
148
  ...(accessMode !== undefined ? { accessMode } : {}),
147
149
  ...(orgMembershipSources !== undefined ? { orgMembershipSources } : {}),
@@ -219,12 +221,14 @@ export class ServerRegistry {
219
221
  this.#servers.set(deploymentId, servedTargetFor(active, built, this.#customerVerifierFactory));
220
222
  this.#activeTenants.set(tenantDeploymentKey(safeTenant, safeServerVersion), deploymentId);
221
223
  this.#activeTenants.delete(tenantKey(safeTenant));
224
+ const activeOwnerSubject = deploymentOwnerSubject(active);
222
225
  return {
223
226
  ok: true,
224
227
  deploymentId,
225
228
  deploymentVersion: version,
226
229
  ...(safeServerVersion !== undefined ? { serverVersion: safeServerVersion } : {}),
227
230
  ...(accessMode !== undefined ? { accessMode } : {}),
231
+ ...(activeOwnerSubject !== undefined ? { ownerSubject: activeOwnerSubject } : {}),
228
232
  };
229
233
  }
230
234
  /** Read-only deploy validation used by the CLI before config writes, asset upload, or persistence. */
@@ -338,8 +342,7 @@ export class ServerRegistry {
338
342
  return {
339
343
  accessMode: record.accessMode,
340
344
  orgMembershipSources: record.orgMembershipSources,
341
- ownerSubject: record.createdBySubject,
342
- ownerEmail: record.createdByEmail,
345
+ ownerSubject: deploymentOwnerSubject(record),
343
346
  };
344
347
  }
345
348
  async getActiveByTenantVersion(ref, serverVersion) {
@@ -415,9 +418,9 @@ export class ServerRegistry {
415
418
  const built = await this.#compilePersistedRecord(record);
416
419
  return deploymentStatusFor(safe, record, built, baseUrl, endpointOptions);
417
420
  }
418
- async updateAccess(ref, accessMode, serverVersion) {
421
+ async updateAccess(ref, options) {
419
422
  const safe = validateTenantRef(ref);
420
- return updateRegistryAccess(this.#stateView(), safe, accessMode, serverVersion, (deploymentId) => this.get(deploymentId), this.#customerVerifierFactory, (record, auth) => this.#hasCustomerAuthAudienceConflict(record, auth));
423
+ return updateRegistryAccess(this.#stateView(), safe, options, (deploymentId) => this.get(deploymentId), this.#customerVerifierFactory, (record, auth) => this.#hasCustomerAuthAudienceConflict(record, auth));
421
424
  }
422
425
  async setDeploymentLock(ref, serverVersion, expectedDeploymentId, deploymentLock) {
423
426
  return setRegistryDeploymentLock(this.#stateView(), validateTenantRef(ref), serverVersion, expectedDeploymentId, deploymentLock);