@noodleseed/one 0.140.0 → 0.141.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/bin.js +0 -0
  2. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +3 -3
  3. package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +9 -1
  4. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  5. package/node_modules/@noodle-borg/app-package/dist/index.d.ts +1 -0
  6. package/node_modules/@noodle-borg/app-package/dist/index.js +1 -0
  7. package/node_modules/@noodle-borg/{compiler → app-package}/dist/schema-validation.d.ts +2 -1
  8. package/node_modules/@noodle-borg/app-package/package.json +2 -0
  9. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-app-sandbox.d.ts +13 -0
  10. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-app-sandbox.js +204 -0
  11. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.d.ts +22 -0
  12. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.js +10 -7
  13. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-transcript.d.ts +18 -0
  14. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-transcript.js +6 -0
  15. package/node_modules/@noodle-borg/assistant-gateway/dist/elevation.d.ts +7 -0
  16. package/node_modules/@noodle-borg/assistant-gateway/dist/elevation.js +1 -0
  17. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +2 -0
  18. package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +2 -0
  19. package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.d.ts +21 -0
  20. package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.js +39 -0
  21. package/node_modules/@noodle-borg/assistant-gateway/dist/session-resume.d.ts +7 -0
  22. package/node_modules/@noodle-borg/assistant-gateway/dist/session-resume.js +12 -0
  23. package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
  24. package/node_modules/@noodle-borg/auth/dist/control-plane-token-exchange.d.ts +110 -0
  25. package/node_modules/@noodle-borg/auth/dist/control-plane-token-exchange.js +217 -0
  26. package/node_modules/@noodle-borg/auth/dist/index.d.ts +1 -0
  27. package/node_modules/@noodle-borg/auth/dist/index.js +1 -0
  28. package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +5 -0
  29. package/node_modules/@noodle-borg/compiler/dist/index.d.ts +1 -2
  30. package/node_modules/@noodle-borg/compiler/dist/index.js +1 -2
  31. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +16 -0
  32. package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +19 -2
  33. package/node_modules/@noodle-borg/compiler/package.json +0 -2
  34. package/node_modules/@noodle-borg/compute/dist/code-connector.js +44 -1
  35. package/node_modules/@noodle-borg/compute/dist/quickjs-engine.js +82 -266
  36. package/node_modules/@noodle-borg/compute/dist/sandbox-eval.js +276 -0
  37. package/node_modules/@noodle-borg/compute/dist/worker-pool.js +262 -0
  38. package/node_modules/@noodle-borg/{service/dist/auth/deploy-gate.d.ts → control-plane/dist/deploy-gates.d.ts} +10 -6
  39. package/node_modules/@noodle-borg/control-plane/dist/deploy-gates.js +266 -0
  40. package/node_modules/@noodle-borg/control-plane/dist/portable.d.ts +1 -0
  41. package/node_modules/@noodle-borg/control-plane/dist/portable.js +1 -0
  42. package/node_modules/@noodle-borg/developer-mcp/dist/capabilities.js +11 -1
  43. package/node_modules/@noodle-borg/developer-mcp/dist/index.js +1 -1
  44. package/node_modules/@noodle-borg/module/dist/platform-human-identity.d.ts +6 -0
  45. package/node_modules/@noodle-borg/module/dist/platform-human-identity.js +14 -0
  46. package/node_modules/@noodle-borg/protocol/dist/execution-error-mapping.js +13 -1
  47. package/node_modules/@noodle-borg/protocol/dist/observation.d.ts +2 -1
  48. package/node_modules/@noodle-borg/protocol/dist/observation.js +2 -1
  49. package/node_modules/@noodle-borg/runtime/dist/connector/failure-details.d.ts +1 -1
  50. package/node_modules/@noodle-borg/runtime/dist/connector/failure-details.js +5 -0
  51. package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +15 -1
  52. package/node_modules/@noodle-borg/runtime/dist/connector/types.js +6 -0
  53. package/node_modules/@noodle-borg/runtime/dist/index.d.ts +1 -1
  54. package/node_modules/@noodle-borg/runtime/dist/index.js +1 -1
  55. package/node_modules/@noodle-borg/runtime/dist/operation-execution.js +1 -0
  56. package/node_modules/@noodle-borg/runtime/dist/state-handles.d.ts +15 -0
  57. package/node_modules/@noodle-borg/runtime/dist/state-handles.js +54 -44
  58. package/node_modules/@noodle-borg/service/dist/auth/developer-grant-guard.js +2 -1
  59. package/node_modules/@noodle-borg/service/dist/modules/route-dispatch.js +1 -1
  60. package/node_modules/@noodle-borg/service/dist/registry-types.d.ts +1 -1
  61. package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +15 -3
  62. package/node_modules/@noodle-borg/service/dist/routes/assistant-clients.js +116 -0
  63. package/node_modules/@noodle-borg/service/dist/routes/assistant-dispatch.js +12 -3
  64. package/node_modules/@noodle-borg/service/dist/routes/assistant-elevation.js +1 -0
  65. package/node_modules/@noodle-borg/service/dist/routes/assistant-interaction-stream.js +4 -1
  66. package/node_modules/@noodle-borg/service/dist/routes/assistant-interactions.js +8 -1
  67. package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +3 -9
  68. package/node_modules/@noodle-borg/service/dist/routes/assistant-route-http.js +11 -0
  69. package/node_modules/@noodle-borg/service/dist/routes/assistant-sandbox.js +12 -47
  70. package/node_modules/@noodle-borg/service/dist/routes/assistant-session-target.js +34 -8
  71. package/node_modules/@noodle-borg/service/dist/routes/assistant-transcript.js +27 -0
  72. package/node_modules/@noodle-borg/service/dist/routes/assistant.js +28 -130
  73. package/node_modules/@noodle-borg/service/dist/routes/config-values.js +1 -1
  74. package/node_modules/@noodle-borg/service/dist/routes/control-plane.js +8 -2
  75. package/node_modules/@noodle-borg/service/dist/service.js +15 -1
  76. package/node_modules/@noodle-borg/service/package.json +1 -1
  77. package/node_modules/@noodle-borg/wire-contracts/dist/assistant.d.ts +17 -0
  78. package/node_modules/@noodle-borg/wire-contracts/dist/assistant.js +25 -0
  79. package/node_modules/@noodleseed/assistant/README.md +4 -9
  80. package/node_modules/@noodleseed/assistant/package.json +1 -1
  81. package/package.json +2 -2
  82. /package/node_modules/@noodle-borg/{compiler → app-package}/dist/schema-validation.js +0 -0
@@ -12,6 +12,16 @@ const TOOL_EXECUTION_ERRORS = new Set([
12
12
  ]);
13
13
  /** Execution-error codes surfaced as JSON-RPC `-32602` (a malformed/unknown request). */
14
14
  const INVALID_PARAMS_ERRORS = new Set(['unknown_tool']);
15
+ /**
16
+ * The allowlisted `connector_error` reasons that reach the wire as structured diagnostics. These
17
+ * are stable classifications (oversized response; execution or queue-wait time-budget expiry) —
18
+ * anything else stays a bare `connector_error` so backend detail never leaks.
19
+ */
20
+ const CONNECTOR_ERROR_STRUCTURED_REASONS = new Set([
21
+ 'response_too_large',
22
+ 'timeout',
23
+ 'queue_timeout',
24
+ ]);
15
25
  /**
16
26
  * Map an execution error to its MCP channel. Call-time, model-correctable failures become an
17
27
  * `isError: true` tool result; an unknown tool is an invalid-params protocol error; everything else
@@ -54,7 +64,9 @@ export function mapExecutionError(error) {
54
64
  result: {
55
65
  content: [{ type: 'text', text }],
56
66
  ...(error.code === 'credential_unavailable' ||
57
- (error.code === 'connector_error' && error.reason === 'response_too_large')
67
+ (error.code === 'connector_error' &&
68
+ error.reason !== undefined &&
69
+ CONNECTOR_ERROR_STRUCTURED_REASONS.has(error.reason))
58
70
  ? { structuredContent: { error: structuredError } }
59
71
  : {}),
60
72
  isError: true,
@@ -21,7 +21,8 @@ export interface ProtocolObservation {
21
21
  }
22
22
  /**
23
23
  * The analytics `errorKind` for an execution error: the stable code, refined with the allowlisted
24
- * `reason` when one exists (`connector_error.timeout`). Analytics-only — wire mapping is unchanged.
24
+ * `reason` when one exists (`connector_error.timeout`). Analytics-only — the wire's structured
25
+ * reasons are gated separately (and more narrowly) in `execution-error-mapping.ts`.
25
26
  */
26
27
  export declare function executionErrorKind(error: {
27
28
  readonly code: string;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * The analytics `errorKind` for an execution error: the stable code, refined with the allowlisted
3
- * `reason` when one exists (`connector_error.timeout`). Analytics-only — wire mapping is unchanged.
3
+ * `reason` when one exists (`connector_error.timeout`). Analytics-only — the wire's structured
4
+ * reasons are gated separately (and more narrowly) in `execution-error-mapping.ts`.
4
5
  */
5
6
  export function executionErrorKind(error) {
6
7
  return error.reason === undefined ? error.code : `${error.code}.${error.reason}`;
@@ -1,5 +1,5 @@
1
1
  import type { ConnectorTraceEvent } from './types.js';
2
- type ConnectorFailureDetails = Pick<ConnectorTraceEvent, 'status' | 'category' | 'attempts' | 'retryable' | 'retryAfterMs' | 'responseExcerpt'>;
2
+ type ConnectorFailureDetails = Pick<ConnectorTraceEvent, 'status' | 'category' | 'attempts' | 'retryable' | 'retryAfterMs' | 'responseExcerpt' | 'queueWaitMs' | 'executionMs'>;
3
3
  /** Reconstruct connector failure metadata from runtime-validated own data properties only. */
4
4
  export declare function sanitizeConnectorFailureDetails(error: unknown, options?: {
5
5
  readonly includeResponseExcerpt?: boolean;
@@ -1,5 +1,6 @@
1
1
  const FAILURE_CATEGORIES = new Set([
2
2
  'timeout',
3
+ 'queue_timeout',
3
4
  'network_error',
4
5
  'rate_limited',
5
6
  'upstream_5xx',
@@ -20,6 +21,8 @@ export function sanitizeConnectorFailureDetails(error, options = {}) {
20
21
  const responseExcerpt = options.includeResponseExcerpt
21
22
  ? ownData(error, 'responseExcerpt')
22
23
  : undefined;
24
+ const queueWaitMs = ownData(error, 'queueWaitMs');
25
+ const executionMs = ownData(error, 'executionMs');
23
26
  return Object.freeze({
24
27
  ...(isIntegerInRange(status, 100, 599) ? { status } : {}),
25
28
  ...(typeof category === 'string' && FAILURE_CATEGORIES.has(category)
@@ -31,6 +34,8 @@ export function sanitizeConnectorFailureDetails(error, options = {}) {
31
34
  ...(typeof responseExcerpt === 'string' && responseExcerpt.length <= MAX_RESPONSE_EXCERPT_LENGTH
32
35
  ? { responseExcerpt }
33
36
  : {}),
37
+ ...(isIntegerInRange(queueWaitMs, 0, MAX_RETRY_AFTER_MS) ? { queueWaitMs } : {}),
38
+ ...(isIntegerInRange(executionMs, 0, MAX_RETRY_AFTER_MS) ? { executionMs } : {}),
34
39
  });
35
40
  }
36
41
  function ownData(error, key) {
@@ -34,11 +34,15 @@ export interface ConnectorTraceEvent {
34
34
  readonly retryable?: boolean;
35
35
  readonly retryAfterMs?: number;
36
36
  readonly responseExcerpt?: string;
37
+ /** Time the call waited for an execution slot, timed independently of execution. */
38
+ readonly queueWaitMs?: number;
39
+ /** Time the call actually executed, excluding queue wait. */
40
+ readonly executionMs?: number;
37
41
  }
38
42
  export interface ExecutionTraceSink {
39
43
  record(event: ConnectorTraceEvent): void;
40
44
  }
41
- export type ConnectorFailureCategory = 'timeout' | 'network_error' | 'rate_limited' | 'upstream_5xx' | 'upstream_4xx' | 'invalid_response' | 'response_too_large';
45
+ export type ConnectorFailureCategory = 'timeout' | 'queue_timeout' | 'network_error' | 'rate_limited' | 'upstream_5xx' | 'upstream_4xx' | 'invalid_response' | 'response_too_large';
42
46
  export declare class ConnectorInvocationError extends Error {
43
47
  readonly status?: number;
44
48
  readonly category?: ConnectorFailureCategory;
@@ -46,6 +50,8 @@ export declare class ConnectorInvocationError extends Error {
46
50
  readonly retryable?: boolean;
47
51
  readonly retryAfterMs?: number;
48
52
  readonly responseExcerpt?: string;
53
+ readonly queueWaitMs?: number;
54
+ readonly executionMs?: number;
49
55
  constructor(message: string, details?: {
50
56
  readonly status?: number;
51
57
  readonly category?: ConnectorFailureCategory;
@@ -53,6 +59,8 @@ export declare class ConnectorInvocationError extends Error {
53
59
  readonly retryable?: boolean;
54
60
  readonly retryAfterMs?: number;
55
61
  readonly responseExcerpt?: string;
62
+ readonly queueWaitMs?: number;
63
+ readonly executionMs?: number;
56
64
  });
57
65
  }
58
66
  /** Trap-free nominal classification for connector rejection values. Proxy wrappers are untrusted. */
@@ -89,6 +97,12 @@ export interface ConnectorCall {
89
97
  readonly route?: CustomerConnectorRoute;
90
98
  /** Optional host capability for sandboxed connectors; ignored by ordinary connectors. */
91
99
  readonly host?: ConnectorCallHost;
100
+ /**
101
+ * Optional execution trace sink for connector-side observability (e.g. sandbox queue-wait vs
102
+ * execution durations). Connectors record only flat scalar fields; the runtime still owns the
103
+ * failure-event path.
104
+ */
105
+ readonly trace?: ExecutionTraceSink;
92
106
  }
93
107
  /**
94
108
  * A connector: a versioned integration exposing typed operations. Phase 1 ships only an in-memory
@@ -7,6 +7,8 @@ export class ConnectorInvocationError extends Error {
7
7
  retryable;
8
8
  retryAfterMs;
9
9
  responseExcerpt;
10
+ queueWaitMs;
11
+ executionMs;
10
12
  constructor(message, details = {}) {
11
13
  super(message);
12
14
  connectorInvocationErrors.add(this);
@@ -24,6 +26,10 @@ export class ConnectorInvocationError extends Error {
24
26
  this.retryAfterMs = details.retryAfterMs;
25
27
  if (details.responseExcerpt !== undefined)
26
28
  this.responseExcerpt = details.responseExcerpt;
29
+ if (details.queueWaitMs !== undefined)
30
+ this.queueWaitMs = details.queueWaitMs;
31
+ if (details.executionMs !== undefined)
32
+ this.executionMs = details.executionMs;
27
33
  }
28
34
  }
29
35
  /** Trap-free nominal classification for connector rejection values. Proxy wrappers are untrusted. */
@@ -18,6 +18,6 @@ export { AllowAllPolicy } from './policy/allow-all.js';
18
18
  export type { PolicyContext, PolicyDecision, PolicyGate } from './policy/types.js';
19
19
  export { type ConfirmationActionReview, type ConfirmationPreparationResult, type ConfirmationReview, type ConnectorFailureAttribution, type ElicitationRequest, type ElicitationResponse, type ExecutionError, type ExecutionErrorCode, type ExecutionResult, type InteractiveExecutionResult, isConfirmationRequired, isInputRequired, isInputRequiredForConfirmation, type PreparedOperationAction, type PreparedToolContinuation, type ToolContinuation, type ToolPreparationContinuation, } from './result.js';
20
20
  export { splitResultMeta } from './result-meta.js';
21
- export { assertNoSecretValue, assertSchemaValue, COMPLETE_STATE_OPERATION, createStateConnector, InMemoryStateHandleStore, PATCH_STATE_OPERATION, READ_STATE_OPERATION, STATE_CONNECTOR_ID, STATE_CONNECTOR_VERSION, STATE_OPERATION_SIGNATURES, StateConnector, type StateHandleRecord, type StateHandleStore, type StateInput, type StateMutationInput, type StatePatchInput, } from './state-handles.js';
21
+ export { assertExpectedStateRevision, assertNoSecretValue, assertSchemaValue, assertStateHandleNotCompleted, COMPLETE_STATE_OPERATION, createMutableStateHandleRecord, createStateConnector, InMemoryStateHandleStore, type MutableStateHandleRecord, PATCH_STATE_OPERATION, READ_STATE_OPERATION, STATE_CONNECTOR_ID, STATE_CONNECTOR_VERSION, STATE_OPERATION_SIGNATURES, StateConnector, type StateHandleRecord, type StateHandleStore, type StateInput, type StateMutationInput, type StatePatchInput, stateHandleRecordForMutation, toPublicStateHandleRecord, } from './state-handles.js';
22
22
  export type { ToolDispatchContext, ToolDispatchDecision, ToolDispatchHook, } from './tool-dispatch.js';
23
23
  //# sourceMappingURL=index.d.ts.map
@@ -15,5 +15,5 @@ export { resolveManagedOrigins, } from './managed-origins.js';
15
15
  export { AllowAllPolicy } from './policy/allow-all.js';
16
16
  export { isConfirmationRequired, isInputRequired, isInputRequiredForConfirmation, } from './result.js';
17
17
  export { splitResultMeta } from './result-meta.js';
18
- export { assertNoSecretValue, assertSchemaValue, COMPLETE_STATE_OPERATION, createStateConnector, InMemoryStateHandleStore, PATCH_STATE_OPERATION, READ_STATE_OPERATION, STATE_CONNECTOR_ID, STATE_CONNECTOR_VERSION, STATE_OPERATION_SIGNATURES, StateConnector, } from './state-handles.js';
18
+ export { assertExpectedStateRevision, assertNoSecretValue, assertSchemaValue, assertStateHandleNotCompleted, COMPLETE_STATE_OPERATION, createMutableStateHandleRecord, createStateConnector, InMemoryStateHandleStore, PATCH_STATE_OPERATION, READ_STATE_OPERATION, STATE_CONNECTOR_ID, STATE_CONNECTOR_VERSION, STATE_OPERATION_SIGNATURES, StateConnector, stateHandleRecordForMutation, toPublicStateHandleRecord, } from './state-handles.js';
19
19
  //# sourceMappingURL=index.js.map
@@ -171,6 +171,7 @@ async function invokeOperation(ref, args, sig, toolName, deps, policy, host, env
171
171
  ...(deps.caller !== undefined ? { caller: deps.caller } : {}),
172
172
  ...(customerRoute === undefined ? {} : { route: customerRoute }),
173
173
  host,
174
+ ...(deps.trace === undefined ? {} : { trace: deps.trace }),
174
175
  });
175
176
  }
176
177
  catch (error) {
@@ -14,6 +14,16 @@ export interface StateHandleRecord {
14
14
  readonly status: 'active' | 'completed' | 'expired';
15
15
  readonly expiresAt?: string;
16
16
  }
17
+ export interface MutableStateHandleRecord {
18
+ handle: string;
19
+ key: string;
20
+ value: Record<string, unknown>;
21
+ revision: number;
22
+ status: 'active' | 'completed';
23
+ createdAt: number;
24
+ updatedAt: number;
25
+ expiresAt?: number;
26
+ }
17
27
  export interface StateHandleStore {
18
28
  read(input: StateInput): Promise<StateHandleRecord> | StateHandleRecord;
19
29
  patch(input: StatePatchInput): Promise<StateHandleRecord> | StateHandleRecord;
@@ -46,6 +56,11 @@ export declare class StateConnector implements Connector {
46
56
  invoke(call: ConnectorCall): Promise<unknown>;
47
57
  }
48
58
  export declare function createStateConnector(state: ArtifactState | undefined): StateConnector | undefined;
59
+ export declare function createMutableStateHandleRecord(handle: string, key: string, ttlSeconds: number | undefined, now: number): MutableStateHandleRecord;
60
+ export declare function stateHandleRecordForMutation(record: MutableStateHandleRecord | undefined, handle: string, key: string, ttlSeconds: number | undefined, now: number): MutableStateHandleRecord;
61
+ export declare function assertStateHandleNotCompleted(record: MutableStateHandleRecord): void;
62
+ export declare function assertExpectedStateRevision(record: MutableStateHandleRecord, expectedRevision: number): void;
63
+ export declare function toPublicStateHandleRecord(record: MutableStateHandleRecord, now: number): StateHandleRecord;
49
64
  export declare function assertNoSecretValue(value: unknown, path?: string): void;
50
65
  export declare function assertSchemaValue(schema: Record<string, unknown>, value: Record<string, unknown>): void;
51
66
  //# sourceMappingURL=state-handles.d.ts.map
@@ -55,18 +55,20 @@ export class InMemoryStateHandleStore {
55
55
  read(input) {
56
56
  const def = this.#definition(input.handle);
57
57
  const key = this.#recordKey(def, input);
58
- return this.#publicRecord(this.#records.get(key) ?? this.#emptyRecord(input.handle, input.key ?? 'default', def));
58
+ const now = this.#now().getTime();
59
+ return toPublicStateHandleRecord(this.#records.get(key) ??
60
+ createMutableStateHandleRecord(input.handle, input.key ?? 'default', def.ttlSeconds, now), now);
59
61
  }
60
62
  patch(input) {
61
63
  const def = this.#definition(input.handle);
62
64
  const key = this.#recordKey(def, input);
63
- const current = this.#records.get(key) ?? this.#emptyRecord(input.handle, input.key ?? 'default', def);
64
- assertActive(current, this.#now().getTime());
65
- assertExpectedRevision(current, input.expectedRevision);
65
+ const now = this.#now().getTime();
66
+ const current = stateHandleRecordForMutation(this.#records.get(key), input.handle, input.key ?? 'default', def.ttlSeconds, now);
67
+ assertStateHandleNotCompleted(current);
68
+ assertExpectedStateRevision(current, input.expectedRevision);
66
69
  assertNoSecretValue(input.value);
67
70
  const value = { ...current.value, ...input.value };
68
71
  assertSchemaValue(def.schema, value);
69
- const now = this.#now().getTime();
70
72
  const next = {
71
73
  ...current,
72
74
  value,
@@ -74,22 +76,23 @@ export class InMemoryStateHandleStore {
74
76
  updatedAt: now,
75
77
  };
76
78
  this.#records.set(key, next);
77
- return this.#publicRecord(next);
79
+ return toPublicStateHandleRecord(next, now);
78
80
  }
79
81
  complete(input) {
80
82
  const def = this.#definition(input.handle);
81
83
  const key = this.#recordKey(def, input);
82
- const current = this.#records.get(key) ?? this.#emptyRecord(input.handle, input.key ?? 'default', def);
83
- assertActive(current, this.#now().getTime());
84
- assertExpectedRevision(current, input.expectedRevision);
84
+ const now = this.#now().getTime();
85
+ const current = stateHandleRecordForMutation(this.#records.get(key), input.handle, input.key ?? 'default', def.ttlSeconds, now);
86
+ assertStateHandleNotCompleted(current);
87
+ assertExpectedStateRevision(current, input.expectedRevision);
85
88
  const next = {
86
89
  ...current,
87
90
  status: 'completed',
88
91
  revision: current.revision + 1,
89
- updatedAt: this.#now().getTime(),
92
+ updatedAt: now,
90
93
  };
91
94
  this.#records.set(key, next);
92
- return this.#publicRecord(next);
95
+ return toPublicStateHandleRecord(next, now);
93
96
  }
94
97
  #definition(handle) {
95
98
  const def = this.#state.handles[handle];
@@ -102,34 +105,6 @@ export class InMemoryStateHandleStore {
102
105
  const owner = def.scope === 'caller' ? (input.callerSubject ?? 'anonymous') : 'deployment';
103
106
  return `${input.handle}\u0000${owner}\u0000${userKey}`;
104
107
  }
105
- #emptyRecord(handle, key, def) {
106
- const now = this.#now().getTime();
107
- return {
108
- handle,
109
- key,
110
- value: {},
111
- revision: 0,
112
- status: 'active',
113
- createdAt: now,
114
- updatedAt: now,
115
- ...(def.ttlSeconds !== undefined ? { expiresAt: now + def.ttlSeconds * 1000 } : {}),
116
- };
117
- }
118
- #publicRecord(record) {
119
- const status = record.expiresAt !== undefined && record.expiresAt <= this.#now().getTime()
120
- ? 'expired'
121
- : record.status;
122
- return {
123
- handle: record.handle,
124
- key: record.key,
125
- value: record.value,
126
- revision: record.revision,
127
- status,
128
- ...(record.expiresAt !== undefined
129
- ? { expiresAt: new Date(record.expiresAt).toISOString() }
130
- : {}),
131
- };
132
- }
133
108
  }
134
109
  export class StateConnector {
135
110
  id = STATE_CONNECTOR_ID;
@@ -212,18 +187,53 @@ function objectArg(value, name) {
212
187
  throw new Error(`${name} must be an object`);
213
188
  return value;
214
189
  }
215
- function assertActive(record, now) {
190
+ export function createMutableStateHandleRecord(handle, key, ttlSeconds, now) {
191
+ return {
192
+ handle,
193
+ key,
194
+ value: {},
195
+ revision: 0,
196
+ status: 'active',
197
+ createdAt: now,
198
+ updatedAt: now,
199
+ ...(ttlSeconds !== undefined ? { expiresAt: now + ttlSeconds * 1000 } : {}),
200
+ };
201
+ }
202
+ export function stateHandleRecordForMutation(record, handle, key, ttlSeconds, now) {
203
+ if (record === undefined)
204
+ return createMutableStateHandleRecord(handle, key, ttlSeconds, now);
205
+ if (!isExpired(record, now))
206
+ return record;
207
+ return {
208
+ ...createMutableStateHandleRecord(handle, key, ttlSeconds, now),
209
+ revision: record.revision + 1,
210
+ };
211
+ }
212
+ export function assertStateHandleNotCompleted(record) {
216
213
  if (record.status === 'completed')
217
214
  throw new Error('completed state handles are read-only');
218
- if (record.expiresAt !== undefined && record.expiresAt <= now) {
219
- throw new Error('expired state handles are read-only');
220
- }
221
215
  }
222
- function assertExpectedRevision(record, expectedRevision) {
216
+ function isExpired(record, now) {
217
+ return record.expiresAt !== undefined && record.expiresAt <= now;
218
+ }
219
+ export function assertExpectedStateRevision(record, expectedRevision) {
223
220
  if (record.revision !== expectedRevision) {
224
221
  throw new Error(`state revision conflict: expected ${expectedRevision}, got ${record.revision}`);
225
222
  }
226
223
  }
224
+ export function toPublicStateHandleRecord(record, now) {
225
+ const expired = isExpired(record, now);
226
+ return {
227
+ handle: record.handle,
228
+ key: record.key,
229
+ value: record.value,
230
+ revision: record.revision + (expired ? 1 : 0),
231
+ status: expired ? 'expired' : record.status,
232
+ ...(record.expiresAt !== undefined
233
+ ? { expiresAt: new Date(record.expiresAt).toISOString() }
234
+ : {}),
235
+ };
236
+ }
227
237
  const SECRET_LIKE_FIELD = /(secret|token|api[_-]?key|password|credential|authorization|cookie)/i;
228
238
  export function assertNoSecretValue(value, path = 'value') {
229
239
  if (Array.isArray(value)) {
@@ -8,7 +8,8 @@ export async function authorizeDeveloperGrant(identity, context) {
8
8
  });
9
9
  if (grant === undefined)
10
10
  return false;
11
- if (developerResourcePath(grant.resource) !== context.resourcePath)
11
+ const grantPath = developerResourcePath(grant.resource);
12
+ if (grantPath === undefined || !context.resourcePaths.has(grantPath))
12
13
  return false;
13
14
  if (!grant.capabilities.includes(context.capability))
14
15
  return false;
@@ -42,7 +42,7 @@ export function createModuleRouteContext(hostRequest, deps) {
42
42
  controlPlane: deps.controlPlane,
43
43
  org: input.org,
44
44
  capability: input.permission,
45
- resourcePath: DEVELOPER_CLI_PATH,
45
+ resourcePaths: new Set([DEVELOPER_CLI_PATH]),
46
46
  }));
47
47
  if (!allowed) {
48
48
  return forbidden('developer grant does not authorize this operation');
@@ -1,11 +1,11 @@
1
1
  import type { CapabilityName } from '@noodle-borg/capabilities';
2
2
  import type { CatalogConnector, HostedPackagedAsset } from '@noodle-borg/compiler';
3
+ import type { ControlPlaneIdentity } from '@noodle-borg/control-plane/portable';
3
4
  import type { OrgMembershipSource, PolicyGate } from '@noodle-borg/module';
4
5
  import type { Connector } from '@noodle-borg/runtime';
5
6
  import type { AccessMode, OwnerTokenVerifier } from '@noodle-borg/transport-http';
6
7
  import type { DeploymentSource } from '@noodle-borg/wire-contracts';
7
8
  import type { AppPackageRenderer } from './app-package-snapshot.js';
8
- import type { ControlPlaneIdentity } from './auth/deploy-gate.js';
9
9
  import type { DelegatedExchangeOptions } from './delegated-token-exchange.js';
10
10
  import type { ExternalCredentialExchangeRuntimeOptions } from './external-credential-exchange.js';
11
11
  import type { GoogleWorkloadIdentityRuntimeOptions } from './google-workload-identity.js';
@@ -1,6 +1,6 @@
1
1
  import { ADMISSION_DEFAULTS } from '@noodle-borg/admission-limits/portable';
2
2
  import { requestModelCompletion, } from '@noodle-borg/assistant-gateway/model-runtime';
3
- import { assistantModelToolOncePerSession, assistantModelToolRequiredWhenVisible, dispatchAssistantTool, projectAssistantGuide, publicSurfaceOf, selectAssistantModelTools, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
3
+ import { assistantModelToolOncePerSession, assistantModelToolRequiredWhenVisible, authenticatedSurfaceOf, dispatchAssistantTool, projectAssistantGuide, publicSurfaceOf, selectAssistantModelTools, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
4
4
  import { validateJsonSchemaWithDefaults } from '@noodle-borg/compiler';
5
5
  import { guardedFetch } from '@noodle-borg/connector-http';
6
6
  import { evaluateToolAuthorization } from '@noodle-borg/protocol';
@@ -383,10 +383,22 @@ function selectTurnModelTools(target, caller, latestMessage, usedToolNames) {
383
383
  ...(usedToolNames === undefined ? {} : { usedToolNames }),
384
384
  });
385
385
  }
386
- /** Surface behavior is injected only when the session is pinned to the public projection that minted it. */
386
+ /**
387
+ * Surface behavior is injected only for the exact surface the session is bound to (ADR 0215): the
388
+ * public-audience surface for embed-minted sessions, the authenticated surface for backend-exchanged
389
+ * ones. A session with no binding — a record from before binding existed, or a pre-surfaces artifact —
390
+ * fails closed to no surface block, never to a union or first-surface fallback.
391
+ */
387
392
  function surfaceInstructionsContext(assistant, session) {
388
- if (session.publicEmbedId === undefined)
393
+ const bound = session.boundSurface ?? (session.publicEmbedId !== undefined ? 'public' : undefined);
394
+ if (bound === undefined)
389
395
  return '';
396
+ if (bound === 'authenticated') {
397
+ const surface = authenticatedSurfaceOf(assistant);
398
+ if (!surface?.instructions)
399
+ return '';
400
+ return `\n\nSurface instructions (authenticated website surface; same trust level as tenant instructions):\n${surface.instructions}`;
401
+ }
390
402
  const surface = publicSurfaceOf(assistant);
391
403
  if (!surface?.instructions)
392
404
  return '';
@@ -0,0 +1,116 @@
1
+ import { readJsonBody, sendForbidden, sendJson } from '../http-util.js';
2
+ import { now } from './assistant-route-http.js';
3
+ import { authorizeControlPlane } from './control-plane.js';
4
+ export async function handleAssistantClients(req, res, tenant, id, action, deps) {
5
+ const identity = await authorizeControlPlane(req, res, deps.gate, { requireIdentity: false });
6
+ if (identity === false)
7
+ return;
8
+ if (identity && !identity.superAdmin) {
9
+ const member = await deps.controlPlane.isOrgMember({
10
+ org: tenant.org,
11
+ subject: identity.subject,
12
+ });
13
+ if (!member)
14
+ return sendForbidden(res, 'forbidden');
15
+ }
16
+ if (action === 'collection' && req.method === 'GET') {
17
+ const clients = await deps.store.listClients(tenant);
18
+ return sendJson(res, 200, {
19
+ ok: true,
20
+ clients: clients.map(publicClient),
21
+ });
22
+ }
23
+ if (action === 'collection' && req.method === 'POST') {
24
+ const body = await readJsonBody(req, deps.maxBody);
25
+ if (!body.ok)
26
+ return sendJson(res, body.status, { error: body.error });
27
+ const name = body.value.name;
28
+ if (typeof name !== 'string' || name.trim().length < 1 || name.length > 80) {
29
+ return sendJson(res, 400, { error: '"name" must be a non-empty string' });
30
+ }
31
+ const target = await deps.registry.getActiveByTenant(tenant);
32
+ const assistant = target?.served.artifact.server.assistant;
33
+ if (!target?.deploymentId || !assistant)
34
+ return sendJson(res, 409, { error: 'deployment has no embedded assistant' });
35
+ const created = await deps.store.createClient({
36
+ name: name.trim(),
37
+ tenant,
38
+ deploymentId: target.deploymentId,
39
+ allowedOrigins: assistant.allowedOrigins,
40
+ now: now(deps),
41
+ });
42
+ await deps.audit.emit({
43
+ eventType: 'assistant.client.created',
44
+ org: tenant.org,
45
+ app: tenant.app,
46
+ env: tenant.env,
47
+ deploymentId: target.deploymentId,
48
+ decision: 'allow',
49
+ status: 201,
50
+ ...(identity ? { actorSubject: identity.subject } : {}),
51
+ ...(identity?.email ? { actorEmail: identity.email } : {}),
52
+ details: { clientId: created.client.id, name: created.client.name },
53
+ });
54
+ return sendJson(res, 201, {
55
+ ok: true,
56
+ ...publicClient(created.client),
57
+ clientSecret: created.secret,
58
+ });
59
+ }
60
+ if (!id)
61
+ return sendJson(res, 404, { error: 'not found' });
62
+ const owned = (await deps.store.listClients(tenant)).some((client) => client.id === id);
63
+ if (!owned)
64
+ return sendJson(res, 404, { error: 'not found' });
65
+ if (action === 'rotate' && req.method === 'POST') {
66
+ const rotated = await deps.store.rotateClient(id, now(deps));
67
+ if (!rotated)
68
+ return sendJson(res, 404, { error: 'not found' });
69
+ await deps.audit.emit({
70
+ eventType: 'assistant.client.rotated',
71
+ org: tenant.org,
72
+ app: tenant.app,
73
+ env: tenant.env,
74
+ deploymentId: rotated.client.deploymentId,
75
+ decision: 'allow',
76
+ status: 200,
77
+ ...(identity ? { actorSubject: identity.subject } : {}),
78
+ ...(identity?.email ? { actorEmail: identity.email } : {}),
79
+ details: { clientId: rotated.client.id },
80
+ });
81
+ return sendJson(res, 200, {
82
+ ok: true,
83
+ ...publicClient(rotated.client),
84
+ clientSecret: rotated.secret,
85
+ });
86
+ }
87
+ if (action === 'item' && req.method === 'DELETE') {
88
+ await deps.store.revokeClient(id, now(deps));
89
+ await deps.audit.emit({
90
+ eventType: 'assistant.client.revoked',
91
+ org: tenant.org,
92
+ app: tenant.app,
93
+ env: tenant.env,
94
+ decision: 'allow',
95
+ status: 204,
96
+ ...(identity ? { actorSubject: identity.subject } : {}),
97
+ ...(identity?.email ? { actorEmail: identity.email } : {}),
98
+ details: { clientId: id },
99
+ });
100
+ res.statusCode = 204;
101
+ res.end();
102
+ return;
103
+ }
104
+ sendJson(res, 405, { error: 'method not allowed' });
105
+ }
106
+ function publicClient(client) {
107
+ return {
108
+ id: client.id,
109
+ name: client.name,
110
+ createdAt: client.createdAt,
111
+ // Audit data only: sessions follow the tenant's active deployment, not this snapshot.
112
+ createdAgainstDeploymentId: client.deploymentId,
113
+ ...(client.revokedAt ? { revokedAt: client.revokedAt } : {}),
114
+ };
115
+ }
116
+ //# sourceMappingURL=assistant-clients.js.map
@@ -1,10 +1,13 @@
1
- import { handleAssistantAppRequest, handleAssistantClients, handleAssistantPreflight, handleAssistantSession, handleAssistantTurn, handleConsoleApprovalNonce, } from './assistant.js';
1
+ import { isAssistantSandboxHost, } from '@noodle-borg/assistant-gateway/portable';
2
+ import { handleAssistantAppRequest, handleAssistantPreflight, handleAssistantSession, handleAssistantTurn, handleConsoleApprovalNonce, } from './assistant.js';
3
+ import { handleAssistantClients } from './assistant-clients.js';
2
4
  import { handleAssistantDoctor } from './assistant-doctor.js';
3
5
  import { handleAssistantEmbedScript } from './assistant-embed-script.js';
4
6
  import { handleAssistantEmbeds } from './assistant-embeds.js';
5
7
  import { handleAssistantConfirmation, handleAssistantInteraction, } from './assistant-interactions.js';
6
8
  import { handlePublicAssistantSession } from './assistant-public-session.js';
7
9
  import { handleAssistantSandbox } from './assistant-sandbox.js';
10
+ import { handleAssistantTranscript } from './assistant-transcript.js';
8
11
  export function dispatchAssistantRoutes(req, res, url, deps) {
9
12
  if (url.pathname === '/v1/console/approval-nonces/consume' && req.method === 'POST') {
10
13
  return run(req, res, deps, () => handleConsoleApprovalNonce(req, res, deps));
@@ -13,7 +16,8 @@ export function dispatchAssistantRoutes(req, res, url, deps) {
13
16
  url.pathname === '/v1/assistant/turns' ||
14
17
  url.pathname === '/v1/assistant/tool-confirmations' ||
15
18
  url.pathname === '/v1/assistant/interactions' ||
16
- url.pathname === '/v1/assistant/apps') &&
19
+ url.pathname === '/v1/assistant/apps' ||
20
+ url.pathname === '/v1/assistant/transcript') &&
17
21
  req.method === 'OPTIONS') {
18
22
  deps.applySecurityHeaders(res, deps.tls);
19
23
  handleAssistantPreflight(req, res);
@@ -32,7 +36,9 @@ export function dispatchAssistantRoutes(req, res, url, deps) {
32
36
  deps.applySecurityHeaders(res, deps.tls);
33
37
  if (deps.enforceHttps(req, res, deps.tls))
34
38
  return true;
35
- handleAssistantSandbox(req, res);
39
+ const sandboxBase = deps.sandboxBase?.(req);
40
+ const isolatedOrigin = isAssistantSandboxHost(req.headers.host, sandboxBase);
41
+ handleAssistantSandbox(req, res, { isolatedOrigin });
36
42
  return true;
37
43
  }
38
44
  if (url.pathname === '/v1/assistant/public-sessions' && req.method === 'POST') {
@@ -47,6 +53,9 @@ export function dispatchAssistantRoutes(req, res, url, deps) {
47
53
  if (url.pathname === '/v1/assistant/apps' && req.method === 'POST') {
48
54
  return run(req, res, deps, () => handleAssistantAppRequest(req, res, deps));
49
55
  }
56
+ if (url.pathname === '/v1/assistant/transcript' && req.method === 'POST') {
57
+ return run(req, res, deps, () => handleAssistantTranscript(req, res, deps));
58
+ }
50
59
  const doctorMatch = /^\/v1\/orgs\/([^/]+)\/apps\/([^/]+)\/envs\/([^/]+)\/assistant\/doctor$/.exec(url.pathname);
51
60
  if (doctorMatch) {
52
61
  const tenant = decodeTenant(doctorMatch);
@@ -93,6 +93,7 @@ export async function elevateAssistantSession(res, deps, input) {
93
93
  clientId: input.client.id,
94
94
  origin: input.origin,
95
95
  ...(input.customerRouting ? { customerRouting: input.customerRouting } : {}),
96
+ ...(input.boundSurface ? { boundSurface: input.boundSurface } : {}),
96
97
  resume: input.resume,
97
98
  }, {
98
99
  elevations: deps.elevations,
@@ -5,7 +5,10 @@ export async function narrateResolvedInteraction(res, deps, target, session, too
5
5
  try {
6
6
  const narration = await narrateInteractionResolution(target, session, tool, action, result, context, deps, (delta) => res.write(`event: content\ndata: ${JSON.stringify({ delta })}\n\n`));
7
7
  if (narration) {
8
- await deps.store.appendHistory(session.id, [{ role: 'assistant', content: narration }]);
8
+ // Streamed to the panel as content deltas above, so it is genuine visible prose.
9
+ await deps.store.appendHistory(session.id, [
10
+ { role: 'assistant', content: narration, kind: 'visible' },
11
+ ]);
9
12
  }
10
13
  }
11
14
  catch {
@@ -341,7 +341,12 @@ async function finishExecution(res, deps, session, interaction, context, result,
341
341
  status: 'succeeded',
342
342
  });
343
343
  await deps.store.appendHistory(session.id, [
344
- { role: 'assistant', content: `Completed ${toolName}: ${JSON.stringify(safeOutput)}` },
344
+ // Model-facing scaffolding: the panel saw the tool_completed card, never this row's tool JSON.
345
+ {
346
+ role: 'assistant',
347
+ content: `Completed ${toolName}: ${JSON.stringify(safeOutput)}`,
348
+ kind: 'narration',
349
+ },
345
350
  ]);
346
351
  writeInteractionSseHeaders(res, session.origin);
347
352
  writeInteractionResolved(res, interaction.id, 'accept');
@@ -412,9 +417,11 @@ async function resolveStop(res, deps, session, interaction, action) {
412
417
  });
413
418
  const toolName = assistantInteractionTool(completion.interaction) ?? 'tool request';
414
419
  await deps.store.appendHistory(session.id, [
420
+ // Model-facing scaffolding: the panel saw the interaction resolve, never this summary row.
415
421
  {
416
422
  role: 'assistant',
417
423
  content: `${action === 'decline' ? 'Declined' : 'Cancelled'} ${toolName}; it was not executed.`,
424
+ kind: 'narration',
418
425
  },
419
426
  ]);
420
427
  writeInteractionSseHeaders(res, session.origin);