@noodleseed/one 0.164.0 → 0.165.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 (83) hide show
  1. package/dist/commands/solutions-coordination.d.ts +5 -0
  2. package/dist/commands/solutions-coordination.d.ts.map +1 -0
  3. package/dist/commands/solutions-coordination.js +94 -0
  4. package/dist/commands/solutions-coordination.js.map +1 -0
  5. package/dist/commands/solutions-ops.d.ts.map +1 -1
  6. package/dist/commands/solutions-ops.js +8 -4
  7. package/dist/commands/solutions-ops.js.map +1 -1
  8. package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js +2 -0
  9. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +3 -3
  10. package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +2 -0
  11. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +4 -1
  12. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.d.ts +4 -0
  13. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.d.ts +4 -0
  14. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.js +6 -2
  15. package/node_modules/@noodle-borg/authoring/dist/config.d.ts +8 -2
  16. package/node_modules/@noodle-borg/authoring/dist/config.js +28 -1
  17. package/node_modules/@noodle-borg/authoring/dist/connectors.d.ts +37 -0
  18. package/node_modules/@noodle-borg/authoring/dist/connectors.js +4 -0
  19. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -1
  20. package/node_modules/@noodle-borg/authoring/dist/server.js +1 -0
  21. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-flags.js +83 -0
  22. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solutions.js +70 -92
  23. package/node_modules/@noodle-borg/compiler/dist/artifact/operation-ref.d.ts +2 -0
  24. package/node_modules/@noodle-borg/compiler/dist/artifact/version.d.ts +4 -1
  25. package/node_modules/@noodle-borg/compiler/dist/artifact/version.js +4 -1
  26. package/node_modules/@noodle-borg/compiler/dist/catalog/types.d.ts +7 -0
  27. package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
  28. package/node_modules/@noodle-borg/compiler/dist/fulfilment-emit.js +15 -2
  29. package/node_modules/@noodle-borg/compiler/dist/nested-operations.js +71 -0
  30. package/node_modules/@noodle-borg/compute/dist/code-connector.js +14 -2
  31. package/node_modules/@noodle-borg/compute/dist/deterministic-helpers.js +138 -0
  32. package/node_modules/@noodle-borg/compute/dist/quickjs-engine.js +48 -0
  33. package/node_modules/@noodle-borg/compute/dist/sandbox-eval.js +39 -5
  34. package/node_modules/@noodle-borg/compute/dist/worker-entry.js +7 -0
  35. package/node_modules/@noodle-borg/compute/dist/worker-pool.js +14 -3
  36. package/node_modules/@noodle-borg/connector-defs/dist/compile-http.js +31 -2
  37. package/node_modules/@noodle-borg/connector-defs/dist/compile.d.ts +1 -1
  38. package/node_modules/@noodle-borg/connector-defs/dist/compile.js +20 -0
  39. package/node_modules/@noodle-borg/connector-defs/dist/schema.d.ts +84 -0
  40. package/node_modules/@noodle-borg/connector-defs/dist/schema.js +44 -1
  41. package/node_modules/@noodle-borg/connector-http/dist/http-connector.d.ts +7 -0
  42. package/node_modules/@noodle-borg/connector-http/dist/http-connector.js +34 -6
  43. package/node_modules/@noodle-borg/connector-http/dist/http-response.d.ts +8 -1
  44. package/node_modules/@noodle-borg/connector-http/dist/http-response.js +19 -10
  45. package/node_modules/@noodle-borg/connector-http/dist/index.d.ts +1 -1
  46. package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +7 -1
  47. package/node_modules/@noodle-borg/runtime/dist/connector-snapshot.js +8 -0
  48. package/node_modules/@noodle-borg/runtime/dist/execute.d.ts +2 -0
  49. package/node_modules/@noodle-borg/runtime/dist/index.d.ts +1 -0
  50. package/node_modules/@noodle-borg/runtime/dist/nested-operation-host.js +25 -0
  51. package/node_modules/@noodle-borg/runtime/dist/operation-coordination-execution.js +50 -0
  52. package/node_modules/@noodle-borg/runtime/dist/operation-coordination.d.ts +34 -0
  53. package/node_modules/@noodle-borg/runtime/dist/operation-evidence.d.ts +4 -0
  54. package/node_modules/@noodle-borg/runtime/dist/operation-execution.js +173 -94
  55. package/node_modules/@noodle-borg/service/dist/application-activity.js +152 -18
  56. package/node_modules/@noodle-borg/service/dist/application-runtime-target.js +15 -2
  57. package/node_modules/@noodle-borg/service/dist/business-information/definition-resolver.js +19 -0
  58. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-store.js +1 -1
  59. package/node_modules/@noodle-borg/service/dist/business-information/managed-solution-blueprints.js +7 -0
  60. package/node_modules/@noodle-borg/service/dist/business-information/managed-solution-executable.js +11 -1
  61. package/node_modules/@noodle-borg/service/dist/business-information/model.js +2 -2
  62. package/node_modules/@noodle-borg/service/dist/business-information/portable.js +1 -1
  63. package/node_modules/@noodle-borg/service/dist/business-information/profiles.js +37 -1
  64. package/node_modules/@noodle-borg/service/dist/credential-binding-index.js +19 -1
  65. package/node_modules/@noodle-borg/service/dist/deployment-execution.js +1 -1
  66. package/node_modules/@noodle-borg/service/dist/operation-coordination.js +185 -0
  67. package/node_modules/@noodle-borg/service/dist/operation-evidence.js +3 -1
  68. package/node_modules/@noodle-borg/service/dist/routes/business-information-activity.js +48 -53
  69. package/node_modules/@noodle-borg/service/dist/routes/business-information-dispatch.js +2 -1
  70. package/node_modules/@noodle-borg/service/dist/routes/business-information-installation.js +51 -17
  71. package/node_modules/@noodle-borg/service/dist/routes/business-information-paths.js +6 -4
  72. package/node_modules/@noodle-borg/service/dist/routes/business-information-staff.js +4 -3
  73. package/node_modules/@noodle-borg/service/dist/routes/business-information.js +11 -6
  74. package/node_modules/@noodle-borg/service/dist/routes/deploy-public-embed.js +7 -2
  75. package/node_modules/@noodle-borg/service/dist/service.js +2 -1
  76. package/node_modules/@noodle-borg/service/dist/solution-installation-activation.js +58 -3
  77. package/node_modules/@noodle-borg/wire-contracts/dist/business-information.d.ts +52 -0
  78. package/node_modules/@noodle-borg/wire-contracts/dist/business-information.js +6 -0
  79. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
  80. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
  81. package/node_modules/@noodle-borg/wire-contracts/dist/operation-coordination.d.ts +59 -0
  82. package/node_modules/@noodle-borg/wire-contracts/dist/operation-coordination.js +51 -0
  83. package/package.json +1 -1
@@ -3,8 +3,10 @@ import { sanitizeConnectorFailureDetails } from './connector/failure-details.js'
3
3
  import { isConnectorInvocationError } from './connector/types.js';
4
4
  import { preflightOperationCustomerRoutes, resolveCustomerActionRouteBindings, resolveCustomerConnectorRoute, } from './customer-routing.js';
5
5
  import { ExpressionEvalError } from './eval/evaluate.js';
6
+ import { nestedOperationHost } from './nested-operation-host.js';
7
+ import { admitCoordinatedAction, beginOperationCoordination, coordinatedParentId, } from './operation-coordination-execution.js';
6
8
  import { acquireOperationCredential } from './operation-credential.js';
7
- import { withOperationEvidence } from './operation-evidence.js';
9
+ import { safeOperationEvidence, withOperationEvidence } from './operation-evidence.js';
8
10
  import { checkSignature, evalExprMap, validateAgainstSchema } from './operation-validation.js';
9
11
  import { samePreparedAction } from './prepared-action-match.js';
10
12
  import { splitResultMeta } from './result-meta.js';
@@ -116,6 +118,8 @@ async function invokeOperation(ref, args, sig, toolName, deps, policy, host, env
116
118
  const initialCancellation = executionCancellation(deps.signal, beforeDispatch);
117
119
  if (initialCancellation)
118
120
  return { ok: false, error: initialCancellation };
121
+ if (sig.type === 'action' && !admitCoordinatedAction(host))
122
+ return fail('policy_denied', 'This coordinated invocation cannot dispatch another external action.');
119
123
  const callKey = operationKey(ref);
120
124
  if (hostCallStack(host).includes(callKey)) {
121
125
  return fail('circular_call_dependency', `circular connector operation call detected at ${callKey}`, outputPath);
@@ -155,6 +159,9 @@ async function invokeOperation(ref, args, sig, toolName, deps, policy, host, env
155
159
  const acquired = await acquireOperationCredential(ref, deps, customerRoute);
156
160
  if (!acquired.ok)
157
161
  return { ok: false, error: acquired.error };
162
+ const afterCredential = executionCancellation(deps.signal, beforeDispatch);
163
+ if (afterCredential)
164
+ return { ok: false, error: afterCredential };
158
165
  const { credential } = acquired;
159
166
  let connectorArgs;
160
167
  try {
@@ -169,6 +176,8 @@ async function invokeOperation(ref, args, sig, toolName, deps, policy, host, env
169
176
  const admitted = admittedExecutionSignal(deps.signal);
170
177
  disposeSignal = admitted.dispose;
171
178
  const execution = operationIdentity(host, deps, toolName, ref, argsPath);
179
+ const parentId = coordinatedParentId(host);
180
+ const coordinationConnection = connector.coordination?.(ref.operation)?.connectionId;
172
181
  const executionBoundMs = connector.executionBoundMs?.(ref.operation);
173
182
  const deadline = executionBoundMs === undefined ? undefined : AbortSignal.timeout(executionBoundMs);
174
183
  const signal = deadline === undefined
@@ -178,6 +187,8 @@ async function invokeOperation(ref, args, sig, toolName, deps, policy, host, env
178
187
  : AbortSignal.any([deadline, admitted.signal]);
179
188
  return await withOperationEvidence(sig.type === 'action' ? deps.operationEvidence : undefined, {
180
189
  id: execution.id,
190
+ ...(parentId === undefined ? {} : { parentId }),
191
+ ...(coordinationConnection === undefined ? {} : { connectionId: coordinationConnection }),
181
192
  tool: toolName,
182
193
  operation: ref,
183
194
  arguments: connectorArgs,
@@ -187,108 +198,158 @@ async function invokeOperation(ref, args, sig, toolName, deps, policy, host, env
187
198
  : { executionRevision: deps.executionBinding.revision }),
188
199
  ...(executionBoundMs === undefined ? {} : { executionBoundMs }),
189
200
  }, async (reportOutcome) => {
190
- let output;
191
- try {
192
- if (signal?.aborted)
193
- return fail('execution_cancelled', 'Execution deadline elapsed before connector dispatch.');
194
- output = await connector.invoke({
195
- operation: ref.operation,
196
- execution,
197
- reportOutcome,
198
- ...(deps.publicAdmission === undefined
199
- ? {}
200
- : { publicAdmission: deps.publicAdmission }),
201
- args: connectorArgs,
202
- env,
203
- ...(signal === undefined ? {} : { signal }),
204
- credential,
205
- acquireTransportCredential: async () => {
206
- const { credentialBinding: _accountBinding, ...transportRef } = ref;
207
- const transport = await acquireOperationCredential(transportRef, deps, customerRoute);
208
- if (!transport.ok)
209
- throw new Error('Independent transport credential unavailable');
210
- return transport.credential;
211
- },
212
- ...(ref.credentialBinding === undefined
213
- ? {}
214
- : { credentialPresentation: ref.credentialBinding.presentation }),
215
- ...(deps.caller !== undefined ? { caller: deps.caller } : {}),
216
- ...(customerRoute === undefined ? {} : { route: customerRoute }),
217
- host,
218
- ...(deps.trace === undefined ? {} : { trace: deps.trace }),
219
- });
220
- }
221
- catch (error) {
222
- const hostCall = hostCallErrorSnapshot(error);
223
- if (hostCall !== undefined) {
224
- return fail(hostCall.code, hostCall.message, hostCall.path);
225
- }
226
- if (isConnectorInvocationError(error)) {
227
- const failureDetails = sanitizeConnectorFailureDetails(error, {
228
- includeResponseExcerpt: customerRoute === undefined,
201
+ const declaration = connector.coordination?.(ref.operation);
202
+ let coordinated;
203
+ let outcome;
204
+ if (declaration) {
205
+ try {
206
+ coordinated = await beginOperationCoordination({
207
+ declaration,
208
+ executionId: execution.id,
209
+ executionBoundMs,
210
+ args: connectorArgs,
211
+ env,
212
+ deps,
213
+ host,
229
214
  });
230
- deps.trace?.record({
231
- kind: 'connector',
232
- connectorId: ref.connectorId,
233
- connectorVersion: ref.connectorVersion,
215
+ }
216
+ catch {
217
+ reportOutcome({ outcome: 'rejected' });
218
+ return fail('execution_admission_error', 'External operation coordination unavailable; no action dispatched.');
219
+ }
220
+ }
221
+ try {
222
+ let output;
223
+ try {
224
+ if (signal?.aborted)
225
+ return fail('execution_cancelled', 'Execution deadline elapsed before connector dispatch.');
226
+ output = await connector.invoke({
234
227
  operation: ref.operation,
235
- ...failureDetails,
228
+ execution,
229
+ reportOutcome: (value) => {
230
+ outcome = safeOperationEvidence(value);
231
+ reportOutcome(outcome);
232
+ },
233
+ ...(coordinated === undefined
234
+ ? {}
235
+ : {
236
+ coordination: {
237
+ acquired: coordinated.lease.acquired,
238
+ ...(coordinated.lease.previous
239
+ ? { previous: coordinated.lease.previous }
240
+ : {}),
241
+ },
242
+ resolveCoordination: () => {
243
+ if (signal?.aborted)
244
+ return Promise.reject(new Error('Coordination recovery expired'));
245
+ return coordinated.lease.resolvePrevious();
246
+ },
247
+ }),
248
+ ...(deps.publicAdmission === undefined
249
+ ? {}
250
+ : { publicAdmission: deps.publicAdmission }),
251
+ args: connectorArgs,
252
+ env,
253
+ ...(signal === undefined ? {} : { signal }),
254
+ credential,
255
+ acquireTransportCredential: async () => {
256
+ const { credentialBinding: _accountBinding, ...transportRef } = ref;
257
+ const transport = await acquireOperationCredential(transportRef, deps, customerRoute);
258
+ if (!transport.ok)
259
+ throw new Error('Independent transport credential unavailable');
260
+ return transport.credential;
261
+ },
262
+ ...(ref.credentialBinding === undefined
263
+ ? {}
264
+ : { credentialPresentation: ref.credentialBinding.presentation }),
265
+ ...(deps.caller !== undefined ? { caller: deps.caller } : {}),
266
+ ...(customerRoute === undefined ? {} : { route: customerRoute }),
267
+ host: nestedOperationHost(ref, host, signal),
268
+ ...(deps.trace === undefined ? {} : { trace: deps.trace }),
236
269
  });
237
- // Attribution keeps the sanitized classification (never the excerpt) so analytics can name
238
- // the failing connector/operation/stage while the wire result stays generic (#1309).
239
- const status = statusClass(failureDetails.status);
240
- const reason = failureDetails.status === 401 || failureDetails.status === 403
241
- ? 'upstream_authentication_lost'
242
- : failureDetails.category;
270
+ }
271
+ catch (error) {
272
+ const hostCall = hostCallErrorSnapshot(error);
273
+ if (hostCall !== undefined) {
274
+ return fail(hostCall.code, hostCall.message, hostCall.path);
275
+ }
276
+ if (isConnectorInvocationError(error)) {
277
+ const failureDetails = sanitizeConnectorFailureDetails(error, {
278
+ includeResponseExcerpt: customerRoute === undefined,
279
+ });
280
+ deps.trace?.record({
281
+ kind: 'connector',
282
+ connectorId: ref.connectorId,
283
+ connectorVersion: ref.connectorVersion,
284
+ operation: ref.operation,
285
+ ...failureDetails,
286
+ });
287
+ // Attribution keeps the sanitized classification (never the excerpt) so analytics can name
288
+ // the failing connector/operation/stage while the wire result stays generic (#1309).
289
+ const status = statusClass(failureDetails.status);
290
+ const reason = failureDetails.status === 401 || failureDetails.status === 403
291
+ ? 'upstream_authentication_lost'
292
+ : failureDetails.category;
293
+ return fail('connector_error', `connector failed for operation "${ref.operation}"`, {
294
+ ...(reason === undefined ? {} : { reason }),
295
+ connector: {
296
+ connectorId: ref.connectorId,
297
+ connectorVersion: ref.connectorVersion,
298
+ operation: ref.operation,
299
+ ...(failureDetails.category === undefined
300
+ ? {}
301
+ : { category: failureDetails.category }),
302
+ ...(status === undefined ? {} : { statusClass: status }),
303
+ ...(failureDetails.attempts === undefined
304
+ ? {}
305
+ : { attempts: failureDetails.attempts }),
306
+ ...(failureDetails.retryable === undefined
307
+ ? {}
308
+ : { retryable: failureDetails.retryable }),
309
+ },
310
+ });
311
+ }
312
+ // Normalize connector failures; never surface backend internals or credentials.
243
313
  return fail('connector_error', `connector failed for operation "${ref.operation}"`, {
244
- ...(reason === undefined ? {} : { reason }),
245
314
  connector: {
246
315
  connectorId: ref.connectorId,
247
316
  connectorVersion: ref.connectorVersion,
248
317
  operation: ref.operation,
249
- ...(failureDetails.category === undefined
250
- ? {}
251
- : { category: failureDetails.category }),
252
- ...(status === undefined ? {} : { statusClass: status }),
253
- ...(failureDetails.attempts === undefined
254
- ? {}
255
- : { attempts: failureDetails.attempts }),
256
- ...(failureDetails.retryable === undefined
257
- ? {}
258
- : { retryable: failureDetails.retryable }),
259
318
  },
260
319
  });
261
320
  }
262
- // Normalize connector failures; never surface backend internals or credentials.
263
- return fail('connector_error', `connector failed for operation "${ref.operation}"`, {
264
- connector: {
265
- connectorId: ref.connectorId,
266
- connectorVersion: ref.connectorVersion,
267
- operation: ref.operation,
268
- },
269
- });
270
- }
271
- if (customerRoute !== undefined) {
272
- const cloned = cloneRoutedOutput(output, customerRoute.baseUrl);
273
- if (!cloned.ok) {
274
- return fail('connector_error', `connector failed for operation "${ref.operation}"`);
321
+ if (customerRoute !== undefined) {
322
+ const cloned = cloneRoutedOutput(output, customerRoute.baseUrl);
323
+ if (!cloned.ok) {
324
+ return fail('connector_error', `connector failed for operation "${ref.operation}"`);
325
+ }
326
+ output = cloned.value;
275
327
  }
276
- output = cloned.value;
277
- }
278
- let redacted;
279
- try {
280
- redacted = await policy.after(context, output);
328
+ let redacted;
329
+ try {
330
+ redacted = await policy.after(context, output);
331
+ }
332
+ catch {
333
+ return fail('policy_error', 'policy after hook failed');
334
+ }
335
+ // Projection metadata is a reserved runtime envelope, not part of the connector's visible output
336
+ // contract. Validate the visible value deeply while preserving policy-processed metadata for the
337
+ // flow/result projection channel.
338
+ const outputError = validateAgainstSchema(splitResultMeta(redacted).visible, sig.output, outputPath, 'output_invalid', 'output field');
339
+ if (outputError)
340
+ return { ok: false, error: outputError };
341
+ return { ok: true, output: redacted };
281
342
  }
282
- catch {
283
- return fail('policy_error', 'policy after hook failed');
343
+ finally {
344
+ try {
345
+ await coordinated?.lease.finish(coordinated.didAdmitAction()
346
+ ? (outcome ?? { outcome: 'unknown' })
347
+ : { outcome: 'rejected' });
348
+ }
349
+ finally {
350
+ coordinated?.dispose();
351
+ }
284
352
  }
285
- // Projection metadata is a reserved runtime envelope, not part of the connector's visible output
286
- // contract. Validate the visible value deeply while preserving policy-processed metadata for the
287
- // flow/result projection channel.
288
- const outputError = validateAgainstSchema(splitResultMeta(redacted).visible, sig.output, outputPath, 'output_invalid', 'output field');
289
- if (outputError)
290
- return { ok: false, error: outputError };
291
- return { ok: true, output: redacted };
292
353
  });
293
354
  }
294
355
  finally {
@@ -301,7 +362,7 @@ export function createHost(toolName, deps, policy, env, callStack, beforeDispatc
301
362
  const host = {
302
363
  [CALL_STACK]: callStack,
303
364
  [INVOCATION]: { id: deps.invocationId ?? randomUUID(), occurrences: new Map() },
304
- async callOperation(ref, args, path) {
365
+ async callOperation(ref, args, path, parentSignal) {
305
366
  const connector = deps.connectors.resolve(ref);
306
367
  if (!connector)
307
368
  throw new HostCallError('connector_unavailable', 'connector unavailable');
@@ -313,11 +374,29 @@ export function createHost(toolName, deps, policy, env, callStack, beforeDispatc
313
374
  const argError = validateAgainstSchema(args, sig.input, `${path}.args`, 'arg_invalid', 'argument');
314
375
  if (argError)
315
376
  throw new HostCallError(argError.code, argError.message, argError.path);
316
- const result = await invokeOperation(ref, args, sig, toolName, deps, policy, this, env, `${path}.args`, `${path}.output`, beforeDispatch);
317
- if (!result.ok) {
318
- throw new HostCallError(result.error.code, result.error.message, result.error.path);
377
+ const cancellation = executionCancellation(deps.signal, beforeDispatch);
378
+ if (cancellation)
379
+ throw new HostCallError(cancellation.code, cancellation.message);
380
+ // A disconnected admitted request must not mask a later hard parent deadline in AbortSignal.any.
381
+ const inherited = parentSignal === undefined ? undefined : admittedExecutionSignal(deps.signal);
382
+ try {
383
+ const nestedDeps = parentSignal === undefined
384
+ ? deps
385
+ : {
386
+ ...deps,
387
+ signal: inherited?.signal === undefined
388
+ ? parentSignal
389
+ : AbortSignal.any([inherited.signal, parentSignal]),
390
+ };
391
+ const result = await invokeOperation(ref, args, sig, toolName, nestedDeps, policy, this, env, `${path}.args`, `${path}.output`, beforeDispatch);
392
+ if (!result.ok) {
393
+ throw new HostCallError(result.error.code, result.error.message, result.error.path);
394
+ }
395
+ return result.output;
396
+ }
397
+ finally {
398
+ inherited?.dispose();
319
399
  }
320
- return result.output;
321
400
  },
322
401
  };
323
402
  return host;
@@ -1,15 +1,21 @@
1
1
  import { createHmac } from 'node:crypto';
2
2
  import { canonicalJson, sha256Canonical } from '@noodle-borg/compiler';
3
+ import { ApplicationActivityListResponseSchema, ApplicationActivityPreviewResponseSchema, ApplicationActivitySettingsResponseSchema, ApplicationActivitySettingsSaveRequestSchema, OperationCoordinationListRequestSchema, OperationCoordinationListResponseSchema, OperationCoordinationResolveRequestSchema, OperationCoordinationResolveResponseSchema, } from '@noodle-borg/wire-contracts';
3
4
  import { resolveApplicationRuntimeTarget } from './application-runtime-target.js';
5
+ import { createOperationCoordinationPort, } from './operation-coordination.js';
4
6
  import { createOperationEvidencePort, } from './operation-evidence.js';
7
+ const POLICY_MESSAGES = {
8
+ activity_conflict: 'History settings or plan changed. Reload before continuing.',
9
+ activity_invalid: 'Activity paging or retention is invalid for the current policy.',
10
+ activity_unavailable: 'Verified activity history policy is unavailable.',
11
+ coordination_invalid: 'Invalid coordination request.',
12
+ coordination_conflict: 'The operation is active, changed, or unavailable. Inspect the current state before reviewing it again.',
13
+ coordination_unavailable: 'Verified coordination state is unavailable.',
14
+ };
5
15
  export class ActivityPolicyError extends Error {
6
16
  code;
7
17
  constructor(code) {
8
- super(code === 'activity_conflict'
9
- ? 'History settings or plan changed. Reload before continuing.'
10
- : code === 'activity_invalid'
11
- ? 'Activity paging or retention is invalid for the current policy.'
12
- : 'Verified activity history policy is unavailable.');
18
+ super(POLICY_MESSAGES[code]);
13
19
  this.code = code;
14
20
  }
15
21
  }
@@ -19,6 +25,124 @@ export class ApplicationActivity {
19
25
  constructor(options) {
20
26
  this.options = options;
21
27
  }
28
+ /** Shared strict operator projection. Callers authorize the live installation grant before and after it. */
29
+ async project(scope, action, input) {
30
+ const coordinating = action.startsWith('coordination');
31
+ const invalid = () => new ActivityPolicyError(coordinating ? 'coordination_invalid' : 'activity_invalid');
32
+ const allowed = action === 'coordination-list'
33
+ ? ['limit', 'beforeResource']
34
+ : action === 'list' || action === 'export'
35
+ ? ['limit', 'cursor']
36
+ : [];
37
+ const parameters = new Map();
38
+ for (const [key, value] of input.parameters) {
39
+ if (!allowed.includes(key) || parameters.has(key))
40
+ throw invalid();
41
+ parameters.set(key, value);
42
+ }
43
+ const mutation = (response, eventType, details) => ({
44
+ response,
45
+ audit: { eventType, details },
46
+ });
47
+ if (action === 'coordination-resolve') {
48
+ const parsed = OperationCoordinationResolveRequestSchema.safeParse(input.body);
49
+ if (!parsed.success)
50
+ throw invalid();
51
+ const store = this.options.coordination;
52
+ if (!store)
53
+ throw new ActivityPolicyError('coordination_unavailable');
54
+ let resolved;
55
+ try {
56
+ resolved = await store.resolve(scope, parsed.data.resource, parsed.data.token, {
57
+ reviewer: input.reviewer,
58
+ reason: parsed.data.reason,
59
+ });
60
+ }
61
+ catch {
62
+ throw new ActivityPolicyError('coordination_unavailable');
63
+ }
64
+ if (!resolved)
65
+ throw new ActivityPolicyError('coordination_conflict');
66
+ return mutation(OperationCoordinationResolveResponseSchema.parse({ ok: true, data: { resolved: true } }), 'config.operation.coordination_resolved', { resource: parsed.data.resource });
67
+ }
68
+ if (action === 'coordination-list') {
69
+ const parsed = OperationCoordinationListRequestSchema.safeParse({
70
+ ...(parameters.has('limit') ? { limit: Number(parameters.get('limit')) } : {}),
71
+ ...(parameters.has('beforeResource')
72
+ ? { beforeResource: parameters.get('beforeResource') }
73
+ : {}),
74
+ });
75
+ if (!parsed.success)
76
+ throw invalid();
77
+ const store = this.options.coordination;
78
+ if (!store)
79
+ throw new ActivityPolicyError('coordination_unavailable');
80
+ try {
81
+ const limit = parsed.data.limit ?? 100;
82
+ const records = await store.list(scope, limit, parsed.data.beforeResource);
83
+ if (records.some((record) => canonicalJson(record.scope) !== canonicalJson(scope)))
84
+ throw new Error('Coordination scope mismatch');
85
+ return {
86
+ response: OperationCoordinationListResponseSchema.parse({
87
+ ok: true,
88
+ data: {
89
+ records: records.map(({ resource, token, reference, operationDigest, startedAt, deadline, state }) => ({
90
+ resource,
91
+ token,
92
+ reference,
93
+ operationDigest,
94
+ startedAt: new Date(startedAt).toISOString(),
95
+ deadline: new Date(deadline).toISOString(),
96
+ state,
97
+ })),
98
+ ...(records.length === limit ? { nextBeforeResource: records.at(-1)?.resource } : {}),
99
+ },
100
+ }),
101
+ };
102
+ }
103
+ catch {
104
+ throw new ActivityPolicyError('coordination_unavailable');
105
+ }
106
+ }
107
+ if (action === 'save-settings') {
108
+ const parsed = ApplicationActivitySettingsSaveRequestSchema.safeParse(input.body);
109
+ if (!parsed.success)
110
+ throw invalid();
111
+ const data = await this.save(scope, parsed.data);
112
+ return mutation(ApplicationActivitySettingsResponseSchema.parse({ ok: true, data }), 'config.activity.retention_changed', { retentionDays: data.retentionDays });
113
+ }
114
+ if (action === 'preview')
115
+ return {
116
+ response: ApplicationActivityPreviewResponseSchema.parse({
117
+ ok: true,
118
+ data: await this.preview(scope),
119
+ }),
120
+ };
121
+ if (action === 'settings')
122
+ return {
123
+ response: ApplicationActivitySettingsResponseSchema.parse({
124
+ ok: true,
125
+ data: (await this.settings(scope, input.canEdit)).projection,
126
+ }),
127
+ };
128
+ const limit = parameters.has('limit') ? Number(parameters.get('limit')) : 50;
129
+ const cursor = parameters.get('cursor');
130
+ if (!Number.isInteger(limit) ||
131
+ limit < 1 ||
132
+ limit > 100 ||
133
+ (cursor !== undefined && (!cursor || cursor.length > 2048)))
134
+ throw invalid();
135
+ return {
136
+ response: ApplicationActivityListResponseSchema.parse({
137
+ ok: true,
138
+ data: await this.page(scope, {
139
+ purpose: action,
140
+ limit,
141
+ ...(cursor === undefined ? {} : { cursor }),
142
+ }),
143
+ }),
144
+ };
145
+ }
22
146
  async settings(scope, canEdit) {
23
147
  const allowance = await this.options.allowance(scope.org);
24
148
  if (!allowance ||
@@ -174,6 +298,19 @@ export class ApplicationActivity {
174
298
  const scope = installation.scope;
175
299
  const revision = target.served.deps.executionBinding?.revision;
176
300
  const epochRevision = sha256Canonical({ revision, epoch: this.options.epoch });
301
+ const authorize = async () => {
302
+ const active = await deps.registry.getActiveByTenant({
303
+ org: scope.org,
304
+ app: scope.app,
305
+ env: scope.env,
306
+ });
307
+ if (!active || active.deploymentId !== target.deploymentId)
308
+ return false;
309
+ const current = await resolveApplicationRuntimeTarget(active, deps.installations, deps.connections?.readGenerations);
310
+ return (!!current &&
311
+ revision !== undefined &&
312
+ current.served.deps.executionBinding?.revision === revision);
313
+ };
177
314
  const evidence = createOperationEvidencePort({
178
315
  ...this.options,
179
316
  scope,
@@ -181,20 +318,16 @@ export class ApplicationActivity {
181
318
  historyDays: async () => (await this.settings(scope, false)).projection.retentionDays,
182
319
  executionBoundMs: (intent) => intent.executionBoundMs,
183
320
  connectionGeneration: (id) => target.served.deps.executionBinding?.connections[id] ?? revision,
184
- authorize: async () => {
185
- const active = await deps.registry.getActiveByTenant({
186
- org: scope.org,
187
- app: scope.app,
188
- env: scope.env,
189
- });
190
- if (!active || active.deploymentId !== target.deploymentId)
191
- return false;
192
- const current = await resolveApplicationRuntimeTarget(active, deps.installations, deps.connections?.readGenerations);
193
- return (!!current &&
194
- revision !== undefined &&
195
- current.served.deps.executionBinding?.revision === revision);
196
- },
321
+ authorize,
197
322
  });
323
+ const coordination = this.options.coordination &&
324
+ createOperationCoordinationPort({
325
+ ...this.options,
326
+ store: this.options.coordination,
327
+ scope,
328
+ authorize,
329
+ connectionGeneration: (id) => target.served.deps.executionBinding?.connections[id],
330
+ });
198
331
  return {
199
332
  ...target,
200
333
  served: {
@@ -202,6 +335,7 @@ export class ApplicationActivity {
202
335
  deps: {
203
336
  ...target.served.deps,
204
337
  operationEvidence: evidence,
338
+ ...(coordination ? { operationCoordination: coordination } : {}),
205
339
  executionBinding: {
206
340
  revision: epochRevision,
207
341
  connections: target.served.deps.executionBinding?.connections ?? {},
@@ -2,7 +2,7 @@ import { sha256Canonical } from '@noodle-borg/compiler';
2
2
  import { resolveManagedOrigins } from '@noodle-borg/runtime';
3
3
  import { projectApplicationBusinessNotice, } from './application-business-notice.js';
4
4
  import { BusinessOnboarding } from './business-onboarding.js';
5
- import { activateSolutionInstallation, } from './solution-installation-activation.js';
5
+ import { activateSolutionInstallation, readSolutionInstallationActivation, } from './solution-installation-activation.js';
6
6
  /** Resolve operator-owned origins from one current configuration snapshot, never a deployment cache. */
7
7
  export async function resolveTargetOrigins(target) {
8
8
  const env = target.served.deps.env;
@@ -86,6 +86,7 @@ export function applicationServingResolver(input) {
86
86
  installation.applicationGeneration === 'pending')) {
87
87
  const activated = await input.activate({
88
88
  installation,
89
+ purpose: 'runtime-refresh',
89
90
  actor: { subject: 'noodle:managed-solution-release', email: '', superAdmin: false },
90
91
  });
91
92
  if (!activated.ok)
@@ -128,6 +129,13 @@ export function createApplicationServingRuntime(registry, providerOptions, contr
128
129
  controlPlane,
129
130
  audit,
130
131
  });
132
+ const readInstallationActivation = (installation) => readSolutionInstallationActivation(installation, {
133
+ registry,
134
+ businessInformationStore,
135
+ options: providerOptions,
136
+ controlPlane,
137
+ audit,
138
+ });
131
139
  const resolveRuntimeTarget = applicationServingResolver({
132
140
  registry,
133
141
  installations: businessInformationStore,
@@ -136,6 +144,11 @@ export function createApplicationServingRuntime(registry, providerOptions, contr
136
144
  activate: activateInstallation,
137
145
  ...(businessOnboarding ? { businessOnboarding } : {}),
138
146
  });
139
- return { activateInstallation, resolveRuntimeTarget, businessOnboarding };
147
+ return {
148
+ activateInstallation,
149
+ readInstallationActivation,
150
+ resolveRuntimeTarget,
151
+ businessOnboarding,
152
+ };
140
153
  }
141
154
  //# sourceMappingURL=application-runtime-target.js.map
@@ -1,5 +1,6 @@
1
1
  import { sha256Canonical } from '@noodle-borg/app-package';
2
2
  import { projectManagedCollectionControls } from '@noodle-borg/compiler';
3
+ import { builtInDefinition } from './profiles.js';
3
4
  /**
4
5
  * Validate and freeze an active deployment as an immutable private solution definition.
5
6
  * Authorization and active-deployment lookup happen before this pure boundary.
@@ -91,4 +92,22 @@ function summaryFields(schema) {
91
92
  function singularize(title) {
92
93
  return title.endsWith('s') && title.length > 1 ? title.slice(0, -1) : title;
93
94
  }
95
+ export async function resolveInstallDefinition(selector, installingOrg, resolvePrivateDefinition) {
96
+ if (selector.kind === 'managed') {
97
+ return builtInDefinition(selector.profileId);
98
+ }
99
+ if (selector.publisherOrg !== installingOrg ||
100
+ selector.publisherOrg === undefined ||
101
+ selector.app === undefined ||
102
+ selector.environment === undefined ||
103
+ selector.deploymentId === undefined) {
104
+ return undefined;
105
+ }
106
+ return resolvePrivateDefinition?.({
107
+ publisherOrg: selector.publisherOrg,
108
+ app: selector.app,
109
+ environment: selector.environment,
110
+ deploymentId: selector.deploymentId,
111
+ });
112
+ }
94
113
  //# sourceMappingURL=definition-resolver.js.map
@@ -65,7 +65,7 @@ export class InMemoryBusinessInformationStore {
65
65
  return this.#lifecycle.setBusinessNotice(input);
66
66
  }
67
67
  async createInstallation(input) {
68
- const normalized = normalizeInstallationInput(input);
68
+ const normalized = normalizeInstallationInput(input, this.#managedDefinition);
69
69
  const key = scopeKey(normalized.scope);
70
70
  return this.#locks.run(`installations:${normalized.scope.org}`, async () => {
71
71
  const existingKey = this.#installationIds.get(installationIdKey(normalized.scope.org, normalized.scope.installationId));
@@ -27,6 +27,13 @@ export function managedSolutionBlueprint(key, release) {
27
27
  if (key === 'b2b_saas')
28
28
  return undefined;
29
29
  const blueprint = BLUEPRINTS[key];
30
+ if ((key === 'travel' || key === 'restaurant') && release === blueprint.release + 1)
31
+ return {
32
+ ...blueprint,
33
+ release,
34
+ followUpContact: true,
35
+ description: `${blueprint.description} Ask for the guest's reply email before submission so staff can follow up. Explain that the business receives this address; never invent it or claim it is verified.`,
36
+ };
30
37
  return blueprint.release === release ? blueprint : undefined;
31
38
  }
32
39
  export function managedSolutionVariables() {