@noodleseed/one 0.163.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 (106) hide show
  1. package/dist/banner.d.ts +6 -0
  2. package/dist/banner.d.ts.map +1 -0
  3. package/dist/banner.js +29 -0
  4. package/dist/banner.js.map +1 -0
  5. package/dist/commands/project-setup.d.ts.map +1 -1
  6. package/dist/commands/project-setup.js +2 -0
  7. package/dist/commands/project-setup.js.map +1 -1
  8. package/dist/commands/solutions-coordination.d.ts +5 -0
  9. package/dist/commands/solutions-coordination.d.ts.map +1 -0
  10. package/dist/commands/solutions-coordination.js +94 -0
  11. package/dist/commands/solutions-coordination.js.map +1 -0
  12. package/dist/commands/solutions-ops.d.ts.map +1 -1
  13. package/dist/commands/solutions-ops.js +8 -4
  14. package/dist/commands/solutions-ops.js.map +1 -1
  15. package/dist/first-run.d.ts.map +1 -1
  16. package/dist/first-run.js +2 -0
  17. package/dist/first-run.js.map +1 -1
  18. package/node_modules/@noodle-borg/agent-kit/dist/curated/error-fixes.js +2 -0
  19. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +3 -3
  20. package/node_modules/@noodle-borg/agent-kit/dist/generated/surface.js +2 -0
  21. package/node_modules/@noodle-borg/agent-kit/dist/index.d.ts +1 -0
  22. package/node_modules/@noodle-borg/agent-kit/dist/index.js +1 -0
  23. package/node_modules/@noodle-borg/agent-kit/dist/plugin-bootstrap-skill.js +10 -0
  24. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +4 -1
  25. package/node_modules/@noodle-borg/agent-kit/dist/welcome-wordmark.d.ts +3 -0
  26. package/node_modules/@noodle-borg/agent-kit/dist/welcome-wordmark.js +10 -0
  27. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  28. package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.d.ts +4 -0
  29. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.d.ts +4 -0
  30. package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-embed-store.js +6 -2
  31. package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
  32. package/node_modules/@noodle-borg/authoring/dist/config.d.ts +8 -2
  33. package/node_modules/@noodle-borg/authoring/dist/config.js +28 -1
  34. package/node_modules/@noodle-borg/authoring/dist/connectors.d.ts +37 -0
  35. package/node_modules/@noodle-borg/authoring/dist/connectors.js +4 -0
  36. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -1
  37. package/node_modules/@noodle-borg/authoring/dist/server.js +1 -0
  38. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solution-flags.js +83 -0
  39. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-solutions.js +70 -92
  40. package/node_modules/@noodle-borg/compiler/dist/artifact/operation-ref.d.ts +2 -0
  41. package/node_modules/@noodle-borg/compiler/dist/artifact/version.d.ts +4 -1
  42. package/node_modules/@noodle-borg/compiler/dist/artifact/version.js +4 -1
  43. package/node_modules/@noodle-borg/compiler/dist/catalog/types.d.ts +7 -0
  44. package/node_modules/@noodle-borg/compiler/dist/errors.d.ts +1 -1
  45. package/node_modules/@noodle-borg/compiler/dist/fulfilment-emit.js +15 -2
  46. package/node_modules/@noodle-borg/compiler/dist/nested-operations.js +71 -0
  47. package/node_modules/@noodle-borg/compute/dist/code-connector.js +14 -2
  48. package/node_modules/@noodle-borg/compute/dist/deterministic-helpers.js +138 -0
  49. package/node_modules/@noodle-borg/compute/dist/quickjs-engine.js +48 -0
  50. package/node_modules/@noodle-borg/compute/dist/sandbox-eval.js +39 -5
  51. package/node_modules/@noodle-borg/compute/dist/worker-entry.js +7 -0
  52. package/node_modules/@noodle-borg/compute/dist/worker-pool.js +14 -3
  53. package/node_modules/@noodle-borg/connector-defs/dist/compile-http.js +31 -2
  54. package/node_modules/@noodle-borg/connector-defs/dist/compile.d.ts +1 -1
  55. package/node_modules/@noodle-borg/connector-defs/dist/compile.js +20 -0
  56. package/node_modules/@noodle-borg/connector-defs/dist/schema.d.ts +84 -0
  57. package/node_modules/@noodle-borg/connector-defs/dist/schema.js +44 -1
  58. package/node_modules/@noodle-borg/connector-http/dist/http-connector.d.ts +7 -0
  59. package/node_modules/@noodle-borg/connector-http/dist/http-connector.js +34 -6
  60. package/node_modules/@noodle-borg/connector-http/dist/http-response.d.ts +8 -1
  61. package/node_modules/@noodle-borg/connector-http/dist/http-response.js +19 -10
  62. package/node_modules/@noodle-borg/connector-http/dist/index.d.ts +1 -1
  63. package/node_modules/@noodle-borg/control-plane/dist/contracts.d.ts +2 -0
  64. package/node_modules/@noodle-borg/control-plane/dist/deploy-gates.js +1 -0
  65. package/node_modules/@noodle-borg/module/dist/contract.d.ts +2 -0
  66. package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +7 -1
  67. package/node_modules/@noodle-borg/runtime/dist/connector-snapshot.js +8 -0
  68. package/node_modules/@noodle-borg/runtime/dist/execute.d.ts +2 -0
  69. package/node_modules/@noodle-borg/runtime/dist/index.d.ts +1 -0
  70. package/node_modules/@noodle-borg/runtime/dist/nested-operation-host.js +25 -0
  71. package/node_modules/@noodle-borg/runtime/dist/operation-coordination-execution.js +50 -0
  72. package/node_modules/@noodle-borg/runtime/dist/operation-coordination.d.ts +34 -0
  73. package/node_modules/@noodle-borg/runtime/dist/operation-evidence.d.ts +4 -0
  74. package/node_modules/@noodle-borg/runtime/dist/operation-execution.js +173 -94
  75. package/node_modules/@noodle-borg/service/dist/application-activity.js +152 -18
  76. package/node_modules/@noodle-borg/service/dist/application-runtime-target.js +15 -2
  77. package/node_modules/@noodle-borg/service/dist/business-information/definition-resolver.js +19 -0
  78. package/node_modules/@noodle-borg/service/dist/business-information/in-memory-store.js +1 -1
  79. package/node_modules/@noodle-borg/service/dist/business-information/managed-solution-blueprints.js +7 -0
  80. package/node_modules/@noodle-borg/service/dist/business-information/managed-solution-executable.js +11 -1
  81. package/node_modules/@noodle-borg/service/dist/business-information/model.js +2 -2
  82. package/node_modules/@noodle-borg/service/dist/business-information/portable.js +1 -1
  83. package/node_modules/@noodle-borg/service/dist/business-information/profiles.js +37 -1
  84. package/node_modules/@noodle-borg/service/dist/credential-binding-index.js +19 -1
  85. package/node_modules/@noodle-borg/service/dist/deployment-execution.js +1 -1
  86. package/node_modules/@noodle-borg/service/dist/operation-coordination.js +185 -0
  87. package/node_modules/@noodle-borg/service/dist/operation-evidence.js +3 -1
  88. package/node_modules/@noodle-borg/service/dist/routes/business-information-activity.js +48 -53
  89. package/node_modules/@noodle-borg/service/dist/routes/business-information-dispatch.js +2 -1
  90. package/node_modules/@noodle-borg/service/dist/routes/business-information-installation.js +51 -17
  91. package/node_modules/@noodle-borg/service/dist/routes/business-information-paths.js +6 -4
  92. package/node_modules/@noodle-borg/service/dist/routes/business-information-staff.js +4 -3
  93. package/node_modules/@noodle-borg/service/dist/routes/business-information.js +11 -6
  94. package/node_modules/@noodle-borg/service/dist/routes/control-plane.js +14 -1
  95. package/node_modules/@noodle-borg/service/dist/routes/deploy-public-embed.js +7 -2
  96. package/node_modules/@noodle-borg/service/dist/service.js +2 -1
  97. package/node_modules/@noodle-borg/service/dist/solution-installation-activation.js +58 -3
  98. package/node_modules/@noodle-borg/service/package.json +1 -1
  99. package/node_modules/@noodle-borg/wire-contracts/dist/business-information.d.ts +52 -0
  100. package/node_modules/@noodle-borg/wire-contracts/dist/business-information.js +6 -0
  101. package/node_modules/@noodle-borg/wire-contracts/dist/index.d.ts +1 -0
  102. package/node_modules/@noodle-borg/wire-contracts/dist/index.js +1 -0
  103. package/node_modules/@noodle-borg/wire-contracts/dist/operation-coordination.d.ts +59 -0
  104. package/node_modules/@noodle-borg/wire-contracts/dist/operation-coordination.js +51 -0
  105. package/node_modules/@noodleseed/assistant/package.json +1 -1
  106. package/package.json +2 -2
@@ -0,0 +1,71 @@
1
+ import { computeSignatureHash } from './catalog/signature.js';
2
+ /** Resolve reachable system-derived dependency identities without guessing between account aliases. */
3
+ export function resolveNestedOperations(input) {
4
+ const calls = input.connector.operationCalls?.[input.operation];
5
+ if (calls === undefined)
6
+ return undefined;
7
+ const identity = JSON.stringify([input.connector.id, input.connector.version, input.operation]);
8
+ const traversal = input.traversal ?? { ancestors: [], budget: { remaining: 1024 } };
9
+ if (!Array.isArray(calls) ||
10
+ traversal.ancestors.includes(identity) ||
11
+ traversal.ancestors.length >= 32 ||
12
+ traversal.budget.remaining-- <= 0) {
13
+ input.errors.push({
14
+ code: 'invalid_connector_call_graph',
15
+ path: input.path,
16
+ message: 'Nested connector calls must be acyclic and within the compiler graph bounds',
17
+ });
18
+ return [];
19
+ }
20
+ const next = { ancestors: [...traversal.ancestors, identity], budget: traversal.budget };
21
+ const output = [];
22
+ const seen = new Set();
23
+ for (const target of calls) {
24
+ if (traversal.budget.remaining-- <= 0) {
25
+ input.errors.push({
26
+ code: 'invalid_connector_call_graph',
27
+ path: input.path,
28
+ message: 'Nested connector call graph exceeds the compiler bound',
29
+ });
30
+ break;
31
+ }
32
+ const targetConnector = input.catalog.get(target.connectorId, target.connectorVersion);
33
+ const signature = targetConnector?.operations[target.operation];
34
+ if (signature === undefined ||
35
+ computeSignatureHash(target.operation, signature) !== target.signatureHash) {
36
+ input.errors.push({
37
+ code: 'invalid_connector_call_graph',
38
+ path: input.path,
39
+ message: 'Nested connector identity or signature does not match the declared catalog',
40
+ });
41
+ continue;
42
+ }
43
+ const key = JSON.stringify([target.connectorId, target.connectorVersion, target.operation]);
44
+ if (seen.has(key))
45
+ continue;
46
+ seen.add(key);
47
+ const aliases = Object.entries(input.declared).filter(([, ref]) => ref.id === target.connectorId && ref.version === target.connectorVersion);
48
+ if (aliases.length > 1) {
49
+ input.errors.push({
50
+ code: 'ambiguous_nested_connector_binding',
51
+ path: input.path,
52
+ message: `Nested connector "${target.connectorId}@${target.connectorVersion}" matches multiple aliases; declare one unambiguous account binding for this connector`,
53
+ });
54
+ continue;
55
+ }
56
+ // Unbound catalog-only dependencies retain their existing behavior; credential-required targets
57
+ // still fail normal profile validation when no exact manifest account binding exists.
58
+ const alias = aliases[0]?.[0] ?? `#${target.connectorId}@${target.connectorVersion}`;
59
+ const declared = aliases.length === 0
60
+ ? {
61
+ ...input.declared,
62
+ [alias]: { id: target.connectorId, version: target.connectorVersion },
63
+ }
64
+ : input.declared;
65
+ const resolved = input.resolve({ connectorAlias: alias, operation: target.operation, args: {}, path: `${input.path}.calls` }, declared, next);
66
+ if (resolved.resolved)
67
+ output.push(resolved);
68
+ }
69
+ return output;
70
+ }
71
+ //# sourceMappingURL=nested-operations.js.map
@@ -24,6 +24,13 @@ export class CodeConnector {
24
24
  signature(operation) {
25
25
  return this.#operations[operation]?.signature;
26
26
  }
27
+ executionBoundMs(operation) {
28
+ const op = this.#operations[operation];
29
+ return op === undefined ? undefined : (op.limits ?? DEFAULT_LIMITS).timeoutMs;
30
+ }
31
+ coordination(operation) {
32
+ return this.#operations[operation]?.coordination;
33
+ }
27
34
  async invoke(call) {
28
35
  const op = this.#operations[call.operation];
29
36
  if (!op)
@@ -31,7 +38,7 @@ export class CodeConnector {
31
38
  const instance = await this.#engine.instantiate(op.module);
32
39
  let timings;
33
40
  try {
34
- const output = await instance.invoke(call.args, op.limits ?? DEFAULT_LIMITS, op.calls ? computeHost(op.calls, call.host) : undefined, (t) => {
41
+ const output = await instance.invoke(call.args, op.limits ?? DEFAULT_LIMITS, computeHost(op.calls ?? {}, call), (t) => {
35
42
  timings = t;
36
43
  });
37
44
  // Queue wait and execution are recorded independently so burst head-of-line blocking is
@@ -79,8 +86,13 @@ function classifyInvocationFailure(error, timings) {
79
86
  }),
80
87
  });
81
88
  }
82
- function computeHost(calls, host) {
89
+ function computeHost(calls, call) {
90
+ const host = call.host;
83
91
  return {
92
+ ...(call.execution ? { execution: call.execution } : {}),
93
+ ...(call.coordination ? { coordination: call.coordination } : {}),
94
+ ...(call.resolveCoordination ? { resolveCoordination: call.resolveCoordination } : {}),
95
+ ...(call.reportOutcome ? { reportOutcome: call.reportOutcome } : {}),
84
96
  callOperation(name, args) {
85
97
  if (!host) {
86
98
  throw new ComputeError('host_call_failed', 'host call unavailable');
@@ -0,0 +1,138 @@
1
+ import { createHash } from 'node:crypto';
2
+ /** Worker-local pure capabilities: bounded arguments and calls, never an ambient host clock. */
3
+ export function installDeterministicHelpers(context) {
4
+ let calls = 0;
5
+ const formatters = new Map();
6
+ const fn = context.newFunction('__explicitHelper', (operationHandle, argsHandle) => {
7
+ try {
8
+ calls += 1;
9
+ if (calls > 4096)
10
+ throw new Error('explicit helper call budget exceeded');
11
+ const operation = context.getString(operationHandle);
12
+ const json = context.getString(argsHandle);
13
+ if (json.length > 16_384)
14
+ throw new Error('explicit helper input exceeds its bound');
15
+ const args = JSON.parse(json);
16
+ if (!Array.isArray(args))
17
+ throw new Error('explicit helper arguments are invalid');
18
+ let result;
19
+ if (operation === 'parse') {
20
+ const value = args[0];
21
+ if (typeof value !== 'string' ||
22
+ value.length > 64 ||
23
+ !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,9})?(?:Z|[+-]\d\d:\d\d)$/.test(value))
24
+ throw new Error('explicit instant is invalid');
25
+ const year = Number(value.slice(0, 4));
26
+ const month = Number(value.slice(5, 7));
27
+ const day = Number(value.slice(8, 10));
28
+ const maxDay = new Date(`${value.slice(0, 7)}-01T00:00:00Z`);
29
+ maxDay.setUTCMonth(month);
30
+ maxDay.setUTCDate(0);
31
+ const millis = Date.parse(value);
32
+ if (!Number.isFinite(millis) ||
33
+ month < 1 ||
34
+ month > 12 ||
35
+ day < 1 ||
36
+ day > maxDay.getUTCDate() ||
37
+ year < 1 ||
38
+ Number(value.slice(11, 13)) > 23 ||
39
+ Number(value.slice(14, 16)) > 59 ||
40
+ Number(value.slice(17, 19)) > 59)
41
+ throw new Error('explicit instant is invalid');
42
+ result = millis;
43
+ }
44
+ else if (operation === 'format' || operation === 'parts') {
45
+ const epoch = args[0];
46
+ if (typeof epoch !== 'number' ||
47
+ !Number.isInteger(epoch) ||
48
+ epoch < -62135596800000 ||
49
+ epoch > 253402300799999)
50
+ throw new Error('explicit instant is invalid');
51
+ if (operation === 'format')
52
+ result = new Date(epoch).toISOString();
53
+ else {
54
+ const zone = args[1];
55
+ if (typeof zone !== 'string' ||
56
+ zone.length < 1 ||
57
+ zone.length > 64 ||
58
+ zone.trim() !== zone)
59
+ throw new Error('explicit time zone is invalid');
60
+ let formatter = formatters.get(zone);
61
+ if (!formatter) {
62
+ if (formatters.size >= 16)
63
+ throw new Error('explicit time zone count exceeds its bound');
64
+ formatter = new Intl.DateTimeFormat('en-US', {
65
+ timeZone: zone,
66
+ weekday: 'short',
67
+ year: 'numeric',
68
+ month: '2-digit',
69
+ day: '2-digit',
70
+ hour: '2-digit',
71
+ minute: '2-digit',
72
+ hourCycle: 'h23',
73
+ });
74
+ formatters.set(zone, formatter);
75
+ }
76
+ const parts = Object.fromEntries(formatter.formatToParts(epoch).map(({ type, value }) => [type, value]));
77
+ const weekday = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].indexOf(parts.weekday ?? '') + 1;
78
+ result = {
79
+ date: `${parts.year}-${parts.month}-${parts.day}`,
80
+ weekday,
81
+ hour: Number(parts.hour),
82
+ minute: Number(parts.minute),
83
+ };
84
+ }
85
+ }
86
+ else if (operation === 'digest') {
87
+ const value = args[0];
88
+ const encoding = args[1] ?? 'hex';
89
+ if (typeof value !== 'string' ||
90
+ value.length > 8192 ||
91
+ (encoding !== 'hex' && encoding !== 'base32hex'))
92
+ throw new Error('explicit digest input is invalid');
93
+ const digest = createHash('sha256').update(value, 'utf8').digest();
94
+ if (encoding === 'hex')
95
+ result = digest.toString('hex');
96
+ else {
97
+ let bits = 0;
98
+ let value = 0;
99
+ let output = '';
100
+ const alphabet = '0123456789abcdefghijklmnopqrstuv';
101
+ for (const byte of digest) {
102
+ value = (value << 8) | byte;
103
+ bits += 8;
104
+ while (bits >= 5) {
105
+ output += alphabet[(value >>> (bits - 5)) & 31];
106
+ bits -= 5;
107
+ }
108
+ }
109
+ if (bits > 0)
110
+ output += alphabet[(value << (5 - bits)) & 31];
111
+ result = output;
112
+ }
113
+ }
114
+ else
115
+ throw new Error('explicit helper is not available');
116
+ return context.newString(JSON.stringify({ ok: true, value: result }));
117
+ }
118
+ catch {
119
+ return context.newString(JSON.stringify({ ok: false, error: 'invalid or exhausted explicit helper request' }));
120
+ }
121
+ });
122
+ context.setProp(context.global, '__explicitHelper', fn);
123
+ fn.dispose();
124
+ }
125
+ export const helperPrelude = `
126
+ var __helper = function(operation, args) {
127
+ var result = JSON.parse(globalThis.__explicitHelper(operation, JSON.stringify(args)));
128
+ if (!result.ok) throw new Error(result.error);
129
+ return result.value;
130
+ };
131
+ var __timeHelpers = Object.freeze({
132
+ parse: function(value) { return __helper('parse', [value]); },
133
+ format: function(value) { return __helper('format', [value]); },
134
+ parts: function(value, timeZone) { return __helper('parts', [value, timeZone]); }
135
+ });
136
+ var __digestHelper = function(value, encoding) { return __helper('digest', [value, encoding]); };
137
+ `;
138
+ //# sourceMappingURL=deterministic-helpers.js.map
@@ -111,6 +111,54 @@ class QuickJsComputeInstance {
111
111
  */
112
112
  function envelopeBridge(host) {
113
113
  return {
114
+ ...(host.execution === undefined ? {} : { execution: { id: host.execution.id } }),
115
+ ...(host.coordination === undefined
116
+ ? {}
117
+ : {
118
+ coordination: {
119
+ acquired: host.coordination.acquired,
120
+ ...(host.coordination.previous
121
+ ? {
122
+ previous: {
123
+ reference: host.coordination.previous.reference,
124
+ operationDigest: host.coordination.previous.operationDigest,
125
+ },
126
+ }
127
+ : {}),
128
+ },
129
+ }),
130
+ async control(name, argsJson) {
131
+ try {
132
+ const args = JSON.parse(argsJson);
133
+ if (name === 'resolveCoordination' && host.resolveCoordination) {
134
+ await host.resolveCoordination();
135
+ }
136
+ else if (name === 'reportOutcome' &&
137
+ host.reportOutcome &&
138
+ args !== null &&
139
+ typeof args === 'object' &&
140
+ !Array.isArray(args)) {
141
+ const evidence = args;
142
+ if (!['completed', 'rejected', 'unknown'].includes(String(evidence.outcome)) ||
143
+ (evidence.reference !== undefined &&
144
+ (typeof evidence.reference !== 'string' || evidence.reference.length > 1024)))
145
+ throw new Error('invalid execution evidence');
146
+ await host.reportOutcome({
147
+ outcome: evidence.outcome,
148
+ ...(typeof evidence.reference === 'string' ? { reference: evidence.reference } : {}),
149
+ });
150
+ }
151
+ else
152
+ throw new Error('host control unavailable');
153
+ return JSON.stringify({ ok: true, output: null });
154
+ }
155
+ catch {
156
+ return JSON.stringify({
157
+ ok: false,
158
+ error: { code: 'host_call_denied', message: 'host control unavailable or invalid' },
159
+ });
160
+ }
161
+ },
114
162
  async callOperation(name, argsJson) {
115
163
  const envelope = await (async () => {
116
164
  try {
@@ -1,4 +1,5 @@
1
1
  import { shouldInterruptAfterDeadline } from 'quickjs-emscripten';
2
+ import { helperPrelude, installDeterministicHelpers } from './deterministic-helpers.js';
2
3
  import { ComputeError, } from './engine.js';
3
4
  /**
4
5
  * Normalize a host-call failure into a bounded error envelope. The code/path pass through only as
@@ -24,6 +25,7 @@ export function runPure(wasm, source, input, limits) {
24
25
  runtime.setInterruptHandler(shouldInterruptAfterDeadline(Date.now() + limits.timeoutMs));
25
26
  const context = runtime.newContext();
26
27
  try {
28
+ installDeterministicHelpers(context);
27
29
  const wrapper = buildWrapper(source, input);
28
30
  const result = context.evalCode(wrapper, 'module.js');
29
31
  if (result.error) {
@@ -47,10 +49,11 @@ export async function runHosted(wasm, source, input, limits, bridge) {
47
49
  runtime.setMemoryLimit(limits.memoryBytes);
48
50
  runtime.setInterruptHandler(shouldInterruptAfterDeadline(Date.now() + limits.timeoutMs));
49
51
  try {
52
+ installDeterministicHelpers(context);
50
53
  installHostCall(context, bridge, limits);
51
54
  if (bridge.log !== undefined)
52
55
  installConsole(context, bridge);
53
- const result = await context.evalCodeAsync(buildWrapper(source, input, true, bridge.log !== undefined), 'module.js');
56
+ const result = await context.evalCodeAsync(buildWrapper(source, input, true, bridge.log !== undefined, bridge.execution, bridge.coordination), 'module.js');
54
57
  if (result.error) {
55
58
  const described = describeError(context, result.error);
56
59
  result.error.dispose();
@@ -91,9 +94,14 @@ async function drainPendingJobs(runtime, context) {
91
94
  * nondeterministic globals so "deterministic by default" holds for the future resume/idempotency
92
95
  * model ([ADR 0004](../../../docs/decisions/0004-code-as-sandboxed-connector.md)).
93
96
  */
94
- function buildWrapper(source, input, hostEnabled = false, consoleEnabled = false) {
97
+ function buildWrapper(source, input, hostEnabled = false, consoleEnabled = false, execution, coordination) {
95
98
  const callOperationPrelude = hostEnabled
96
99
  ? `
100
+ var __control = function(name, args) {
101
+ var result = JSON.parse(globalThis.__hostControl(name, JSON.stringify(args)));
102
+ if (!result.ok) { var error = new Error(result.error.message); error.name = result.error.code; throw error; }
103
+ return result.output;
104
+ };
97
105
  var callOperation = function(name, args) {
98
106
  var __hostEnvelope = JSON.parse(globalThis.__hostCallOperation(String(name), JSON.stringify(args ?? {})));
99
107
  if (!__hostEnvelope.ok) {
@@ -107,9 +115,16 @@ var callOperation = function(name, args) {
107
115
  };
108
116
  `
109
117
  : '';
110
- const handlerArgs = hostEnabled
111
- ? `${jsLiteral(input ?? null)}, { callOperation: callOperation }`
112
- : jsLiteral(input ?? null);
118
+ const handlerArgs = `${jsLiteral(input ?? null)}, Object.freeze({
119
+ time: __timeHelpers, digest: __digestHelper,
120
+ ${hostEnabled
121
+ ? `callOperation: callOperation,
122
+ reportOutcome: function(evidence) { return __control('reportOutcome', evidence); },
123
+ resolveCoordination: function() { return __control('resolveCoordination', {}); },`
124
+ : ''}
125
+ ${coordination === undefined ? '' : `coordination: Object.freeze(${jsLiteral(coordination)}),`}
126
+ ${execution === undefined ? '' : `execution: Object.freeze(${jsLiteral(execution)}),`}
127
+ })`;
113
128
  const consolePrelude = consoleEnabled
114
129
  ? `
115
130
  var console = Object.freeze({
@@ -123,7 +138,9 @@ var console = Object.freeze({
123
138
  : '';
124
139
  const bodyPrefix = `"use strict";
125
140
  delete globalThis.Date;
141
+ delete globalThis.Intl;
126
142
  delete Math.random;
143
+ ${helperPrelude}
127
144
  ${callOperationPrelude}
128
145
  ${consolePrelude}
129
146
  var __handler = (${source});
@@ -141,6 +158,23 @@ var __out = JSON.stringify(__handler(${handlerArgs}));
141
158
  }
142
159
  function installHostCall(context, bridge, limits) {
143
160
  let calls = 0;
161
+ let controls = 0;
162
+ const control = context.newAsyncifiedFunction('__hostControl', async (name, args) => {
163
+ if (++controls > 4 || !bridge.control)
164
+ return context.newString(JSON.stringify({
165
+ ok: false,
166
+ error: { code: 'host_call_denied', message: 'host control budget exceeded' },
167
+ }));
168
+ const argsJson = context.getString(args);
169
+ if (argsJson.length > 2048)
170
+ return context.newString(JSON.stringify({
171
+ ok: false,
172
+ error: { code: 'host_call_denied', message: 'host control input exceeds bound' },
173
+ }));
174
+ return context.newString(await bridge.control(context.getString(name), argsJson));
175
+ });
176
+ context.setProp(context.global, '__hostControl', control);
177
+ control.dispose();
144
178
  const fn = context.newAsyncifiedFunction('__hostCallOperation', async (nameHandle, argsJsonHandle) => {
145
179
  const envelopeJson = await (async () => {
146
180
  if (calls >= limits.maxHostCalls) {
@@ -29,6 +29,13 @@ function startWorker(port) {
29
29
  });
30
30
  function bridgeFor(run) {
31
31
  return {
32
+ ...(run.execution === undefined ? {} : { execution: run.execution }),
33
+ ...(run.coordination === undefined ? {} : { coordination: run.coordination }),
34
+ control: (name, argsJson) => new Promise((resolve) => {
35
+ const callId = nextCallId++;
36
+ pendingHostReplies.set(callId, resolve);
37
+ post({ t: 'host_control', id: run.id, callId, name, argsJson });
38
+ }),
32
39
  callOperation: (name, argsJson) => new Promise((resolve) => {
33
40
  const callId = nextCallId;
34
41
  nextCallId += 1;
@@ -107,6 +107,8 @@ export class ComputeWorkerPool {
107
107
  limits: { ...job.limits, timeoutMs: remainingMs },
108
108
  hostEnabled: job.host !== undefined,
109
109
  consoleEnabled: job.consoleEnabled,
110
+ ...(job.host?.execution === undefined ? {} : { execution: job.host.execution }),
111
+ ...(job.host?.coordination === undefined ? {} : { coordination: job.host.coordination }),
110
112
  };
111
113
  slot.worker.postMessage(message);
112
114
  }
@@ -129,7 +131,7 @@ export class ComputeWorkerPool {
129
131
  this.#release(slot);
130
132
  return;
131
133
  }
132
- if (message.t === 'host_call') {
134
+ if (message.t === 'host_call' || message.t === 'host_control') {
133
135
  const host = active.job.host;
134
136
  const respond = (envelopeJson) => {
135
137
  if (slot.active === active && !active.settled && !slot.dropped) {
@@ -148,8 +150,15 @@ export class ComputeWorkerPool {
148
150
  }));
149
151
  return;
150
152
  }
151
- host
152
- .callOperation(message.name, message.argsJson)
153
+ const invoke = message.t === 'host_control' ? host.control : host.callOperation;
154
+ if (!invoke) {
155
+ respond(JSON.stringify({
156
+ ok: false,
157
+ error: { code: 'host_call_denied', message: 'host control unavailable' },
158
+ }));
159
+ return;
160
+ }
161
+ invoke(message.name, message.argsJson)
153
162
  .then(respond)
154
163
  .catch(() => respond(JSON.stringify({
155
164
  ok: false,
@@ -157,6 +166,8 @@ export class ComputeWorkerPool {
157
166
  })));
158
167
  return;
159
168
  }
169
+ if (message.t !== 'log')
170
+ return;
160
171
  // 'log': deliver to the sink (fire-and-forget failures) and always release the sandbox.
161
172
  const ack = () => {
162
173
  if (slot.active === active && !active.settled && !slot.dropped) {
@@ -139,7 +139,7 @@ export function compileHttpConnector(def, mode, errors, catalog, connectors, sec
139
139
  const pagination = compilePagination(op, `${base}.pagination`, errors);
140
140
  const projection = compileProjection(op, `${base}.projection`, errors);
141
141
  const fake = httpFake(op.fake);
142
- operations[opName] = buildOperation(op, signature, requestAst, responseAst, headersAst, pagination, fake, projection, evidenceAst);
142
+ operations[opName] = buildOperation(op, signature, requestAst, responseAst, headersAst, pagination, fake, projection, evidenceAst, compileStatusResponses(op.responses, `${base}.responses`, errors));
143
143
  // Per-operation auth declares its own secret reference (overrides any connector-level default).
144
144
  addAuthBinding({
145
145
  auth: op.auth,
@@ -268,7 +268,7 @@ function compileRequest(request, path, errors) {
268
268
  function isRecord(value) {
269
269
  return value !== null && typeof value === 'object';
270
270
  }
271
- function buildOperation(op, signature, requestAst, responseAst, headersAst, pagination, fake, projection, evidenceAst) {
271
+ function buildOperation(op, signature, requestAst, responseAst, headersAst, pagination, fake, projection, evidenceAst, responses) {
272
272
  const resilience = httpResilience(op.resilience);
273
273
  return {
274
274
  method: op.method ?? 'GET',
@@ -287,6 +287,7 @@ function buildOperation(op, signature, requestAst, responseAst, headersAst, pagi
287
287
  ...(op.limits !== undefined ? { maxResponseBytes: op.limits.maxResponseBytes } : {}),
288
288
  ...(op.responseType !== undefined ? { responseType: op.responseType } : {}),
289
289
  ...(op.requestEncoding !== undefined ? { requestEncoding: op.requestEncoding } : {}),
290
+ ...(responses !== undefined ? { responses } : {}),
290
291
  ...(evidenceAst === undefined
291
292
  ? {}
292
293
  : {
@@ -321,6 +322,34 @@ function buildOperation(op, signature, requestAst, responseAst, headersAst, pagi
321
322
  : {}),
322
323
  };
323
324
  }
325
+ function compileStatusResponses(responses, path, errors) {
326
+ if (responses === undefined)
327
+ return undefined;
328
+ return Object.fromEntries(Object.entries(responses).map(([status, response]) => {
329
+ const responseAst = compileExprMap(response.response, RESPONSE_ROOTS, `${path}.${status}.response`, errors);
330
+ const evidenceAst = response.evidence === undefined
331
+ ? undefined
332
+ : compileExprMap(response.evidence, new Set(['response']), `${path}.${status}.evidence`, errors);
333
+ return [
334
+ status,
335
+ {
336
+ ...(response.responseType === undefined ? {} : { responseType: response.responseType }),
337
+ mapResponse: (json, args) => evalMap(responseAst, { args, response: json }),
338
+ ...(evidenceAst === undefined
339
+ ? {}
340
+ : {
341
+ evidence: (json) => {
342
+ const value = evalMap(evidenceAst, { response: json });
343
+ return {
344
+ outcome: value.outcome === 'rejected' ? 'rejected' : 'unknown',
345
+ ...(typeof value.reference === 'string' ? { reference: value.reference } : {}),
346
+ };
347
+ },
348
+ }),
349
+ },
350
+ ];
351
+ }));
352
+ }
324
353
  function usesExecutionIdentity(node) {
325
354
  if (node.kind === 'path')
326
355
  return node.root === 'execution';
@@ -1,6 +1,6 @@
1
1
  import { type CatalogConnector } from '@noodle-borg/compiler';
2
2
  import type { Connector } from '@noodle-borg/runtime';
3
- import type { ConnectorCompileError } from './compile-expr.js';
3
+ import { type ConnectorCompileError } from './compile-expr.js';
4
4
  export type { ConnectorCompileError } from './compile-expr.js';
5
5
  /**
6
6
  * A binding from a connector operation to the *named* secret its auth scheme needs. It carries the
@@ -2,6 +2,7 @@ import { computeSignatureHash, } from '@noodle-borg/compiler';
2
2
  import { CodeConnector, DEFAULT_LIMITS, QuickJsComputeEngine, } from '@noodle-borg/compute';
3
3
  import { parse as parseYaml } from 'yaml';
4
4
  import { validateComputeCallGraph } from './call-graph.js';
5
+ import { compileExpr } from './compile-expr.js';
5
6
  import { compileHttpConnector, toSignature } from './compile-http.js';
6
7
  import { compileMcpConnector } from './compile-mcp.js';
7
8
  import { catalogCredentialMetadata } from './credential-profiles.js';
@@ -119,6 +120,16 @@ function compileComputeConnector(def, engine, targets, errors, catalog, connecto
119
120
  module: engine.registerSource(op.code),
120
121
  limits,
121
122
  ...(calls ? { calls } : {}),
123
+ ...(op.coordination
124
+ ? {
125
+ coordination: {
126
+ connectionId: op.coordination.connectionId,
127
+ namespace: op.coordination.namespace,
128
+ key: compileExpr(op.coordination.key, new Set(['args', 'env', 'execution']), `${base}.coordination.key`, errors),
129
+ reference: compileExpr(op.coordination.reference, new Set(['args', 'env', 'execution']), `${base}.coordination.reference`, errors),
130
+ },
131
+ }
132
+ : {}),
122
133
  };
123
134
  }
124
135
  catalog.push({
@@ -127,6 +138,15 @@ function compileComputeConnector(def, engine, targets, errors, catalog, connecto
127
138
  kind: def.kind ?? 'custom',
128
139
  ...catalogCredentialMetadata(def),
129
140
  operations: signatures,
141
+ operationCalls: Object.fromEntries(Object.entries(operations).map(([name, operation]) => [
142
+ name,
143
+ Object.values(operation.calls ?? {}).map(({ connectorId, connectorVersion, operation, signatureHash }) => ({
144
+ connectorId,
145
+ connectorVersion,
146
+ operation,
147
+ signatureHash,
148
+ })),
149
+ ])),
130
150
  });
131
151
  connectors.push(new CodeConnector({ id: def.id, version: def.version, engine, operations }));
132
152
  }