@noodleseed/one 0.139.4 → 0.141.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.
- package/dist/metrics-render.d.ts +8 -0
- package/dist/metrics-render.d.ts.map +1 -1
- package/dist/metrics-render.js +7 -1
- package/dist/metrics-render.js.map +1 -1
- package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +3 -3
- package/node_modules/@noodle-borg/agent-kit/dist/skill-embedded-assistant-ref.js +9 -1
- package/node_modules/@noodle-borg/app-package/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/app-package/dist/index.js +1 -0
- package/node_modules/@noodle-borg/{compiler → app-package}/dist/schema-validation.d.ts +2 -1
- package/node_modules/@noodle-borg/app-package/package.json +2 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-app-sandbox.d.ts +13 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-app-sandbox.js +204 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.d.ts +22 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.js +10 -7
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-transcript.d.ts +18 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-transcript.js +6 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/elevation.d.ts +7 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/elevation.js +1 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +2 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +2 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.d.ts +21 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-surface.js +39 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/session-resume.d.ts +7 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/session-resume.js +12 -0
- package/node_modules/@noodle-borg/auth/dist/control-plane-token-exchange.d.ts +110 -0
- package/node_modules/@noodle-borg/auth/dist/control-plane-token-exchange.js +217 -0
- package/node_modules/@noodle-borg/auth/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/auth/dist/index.js +1 -0
- package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +5 -0
- package/node_modules/@noodle-borg/compiler/dist/index.d.ts +1 -2
- package/node_modules/@noodle-borg/compiler/dist/index.js +1 -2
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +16 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +19 -2
- package/node_modules/@noodle-borg/compiler/package.json +0 -2
- package/node_modules/@noodle-borg/compute/dist/code-connector.js +44 -1
- package/node_modules/@noodle-borg/compute/dist/quickjs-engine.js +82 -266
- package/node_modules/@noodle-borg/compute/dist/sandbox-eval.js +276 -0
- package/node_modules/@noodle-borg/compute/dist/worker-pool.js +262 -0
- package/node_modules/@noodle-borg/{service/dist/auth/deploy-gate.d.ts → control-plane/dist/deploy-gates.d.ts} +10 -6
- package/node_modules/@noodle-borg/control-plane/dist/deploy-gates.js +266 -0
- package/node_modules/@noodle-borg/control-plane/dist/portable.d.ts +1 -0
- package/node_modules/@noodle-borg/control-plane/dist/portable.js +1 -0
- package/node_modules/@noodle-borg/developer-mcp/dist/capabilities.js +11 -1
- package/node_modules/@noodle-borg/developer-mcp/dist/index.js +1 -1
- package/node_modules/@noodle-borg/module/dist/contract.d.ts +0 -64
- package/node_modules/@noodle-borg/module/dist/contract.js +3 -2
- package/node_modules/@noodle-borg/module/dist/index.d.ts +1 -0
- package/node_modules/@noodle-borg/module/dist/index.js +1 -0
- package/node_modules/@noodle-borg/module/dist/intent.d.ts +1 -1
- package/node_modules/@noodle-borg/module/dist/request-analytics.d.ts +80 -0
- package/node_modules/@noodle-borg/module/dist/request-analytics.js +10 -0
- package/node_modules/@noodle-borg/observability/dist/intent-capture.js +4 -0
- package/node_modules/@noodle-borg/observability/dist/request-event-buffer.js +4 -0
- package/node_modules/@noodle-borg/observability/dist/request-event-metrics.js +15 -0
- package/node_modules/@noodle-borg/observability/dist/request-events-postgres.js +19 -3
- package/node_modules/@noodle-borg/observability/dist/request-events.js +3 -0
- package/node_modules/@noodle-borg/observability/dist/runtime.js +61 -1
- package/node_modules/@noodle-borg/protocol/dist/execution-error-mapping.js +13 -1
- package/node_modules/@noodle-borg/protocol/dist/handlers/prompts.js +2 -2
- package/node_modules/@noodle-borg/protocol/dist/handlers/resources.js +2 -2
- package/node_modules/@noodle-borg/protocol/dist/handlers/tools.js +3 -3
- package/node_modules/@noodle-borg/protocol/dist/observation.d.ts +18 -0
- package/node_modules/@noodle-borg/protocol/dist/observation.js +15 -0
- package/node_modules/@noodle-borg/protocol/dist/v2/server.js +5 -5
- package/node_modules/@noodle-borg/runtime/dist/connector/failure-details.d.ts +1 -1
- package/node_modules/@noodle-borg/runtime/dist/connector/failure-details.js +5 -0
- package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +15 -1
- package/node_modules/@noodle-borg/runtime/dist/connector/types.js +6 -0
- package/node_modules/@noodle-borg/runtime/dist/index.d.ts +1 -1
- package/node_modules/@noodle-borg/runtime/dist/operation-execution.d.ts +1 -1
- package/node_modules/@noodle-borg/runtime/dist/operation-execution.js +35 -6
- package/node_modules/@noodle-borg/runtime/dist/result.d.ts +18 -0
- package/node_modules/@noodle-borg/service/dist/auth/developer-grant-guard.js +2 -1
- package/node_modules/@noodle-borg/service/dist/modules/route-dispatch.js +1 -1
- package/node_modules/@noodle-borg/service/dist/registry-types.d.ts +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +15 -3
- package/node_modules/@noodle-borg/service/dist/routes/assistant-clients.js +116 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-dispatch.js +12 -3
- package/node_modules/@noodle-borg/service/dist/routes/assistant-elevation.js +1 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-interaction-stream.js +4 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-interactions.js +8 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +3 -9
- package/node_modules/@noodle-borg/service/dist/routes/assistant-route-http.js +11 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-sandbox.js +12 -47
- package/node_modules/@noodle-borg/service/dist/routes/assistant-session-target.js +34 -8
- package/node_modules/@noodle-borg/service/dist/routes/assistant-transcript.js +27 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant.js +28 -130
- package/node_modules/@noodle-borg/service/dist/routes/config-values.js +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/control-plane.js +8 -2
- package/node_modules/@noodle-borg/service/dist/service-resource-cleanup.js +1 -6
- package/node_modules/@noodle-borg/service/dist/service.js +15 -1
- package/node_modules/@noodle-borg/transport-http/dist/client-identity.js +124 -0
- package/node_modules/@noodle-borg/transport-http/dist/identity-authorization.js +8 -8
- package/node_modules/@noodle-borg/transport-http/dist/request-capture.js +97 -10
- package/node_modules/@noodle-borg/transport-http/dist/serve-request.js +40 -10
- package/node_modules/@noodle-borg/wire-contracts/dist/assistant.d.ts +17 -0
- package/node_modules/@noodle-borg/wire-contracts/dist/assistant.js +25 -0
- package/node_modules/@noodleseed/assistant/README.md +4 -9
- package/package.json +1 -1
- /package/node_modules/@noodle-borg/{compiler → app-package}/dist/schema-validation.js +0 -0
|
@@ -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. */
|
|
@@ -16,7 +16,7 @@ export type { InvocationContext, InvocationContextPreferenceSource, InvocationLo
|
|
|
16
16
|
export { type ManagedOriginResolutionResult, resolveManagedOrigins, } from './managed-origins.js';
|
|
17
17
|
export { AllowAllPolicy } from './policy/allow-all.js';
|
|
18
18
|
export type { PolicyContext, PolicyDecision, PolicyGate } from './policy/types.js';
|
|
19
|
-
export { type ConfirmationActionReview, type ConfirmationPreparationResult, type ConfirmationReview, 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';
|
|
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
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';
|
|
22
22
|
export type { ToolDispatchContext, ToolDispatchDecision, ToolDispatchHook, } from './tool-dispatch.js';
|
|
@@ -29,6 +29,6 @@ export declare function prepareOperationAction(ref: ResolvedOperationRef | {
|
|
|
29
29
|
}, argsAst: ExprMap, scope: EvalScope, deps: ExecuteDeps, argsPath: string, additionalOperationCount?: number): PreparedOperationActionResult;
|
|
30
30
|
/** @internal Shared with the suspension-aware flow executor. */
|
|
31
31
|
export declare function createHost(toolName: string, deps: ExecuteDeps, policy: PolicyGate, env: Record<string, string>, callStack: string[], beforeDispatch?: ToolDispatchHook): ConnectorCallHost;
|
|
32
|
-
export declare function fail(code: ExecutionError['code'], message: string, pathOrDetails?: string | Pick<ExecutionError, 'reason' | 'fix' | 'next'>): ExecutionResult;
|
|
32
|
+
export declare function fail(code: ExecutionError['code'], message: string, pathOrDetails?: string | Pick<ExecutionError, 'reason' | 'fix' | 'next' | 'connector'>): ExecutionResult;
|
|
33
33
|
export {};
|
|
34
34
|
//# sourceMappingURL=operation-execution.d.ts.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) {
|
|
@@ -189,14 +190,32 @@ async function invokeOperation(ref, args, sig, toolName, deps, policy, host, env
|
|
|
189
190
|
operation: ref.operation,
|
|
190
191
|
...failureDetails,
|
|
191
192
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
// Attribution keeps the sanitized classification (never the excerpt) so analytics can name
|
|
194
|
+
// the failing connector/operation/stage while the wire result stays generic (#1309).
|
|
195
|
+
const status = statusClass(failureDetails.status);
|
|
196
|
+
return fail('connector_error', `connector failed for operation "${ref.operation}"`, {
|
|
197
|
+
...(failureDetails.category === undefined ? {} : { reason: failureDetails.category }),
|
|
198
|
+
connector: {
|
|
199
|
+
connectorId: ref.connectorId,
|
|
200
|
+
connectorVersion: ref.connectorVersion,
|
|
201
|
+
operation: ref.operation,
|
|
202
|
+
...(failureDetails.category === undefined ? {} : { category: failureDetails.category }),
|
|
203
|
+
...(status === undefined ? {} : { statusClass: status }),
|
|
204
|
+
...(failureDetails.attempts === undefined ? {} : { attempts: failureDetails.attempts }),
|
|
205
|
+
...(failureDetails.retryable === undefined
|
|
206
|
+
? {}
|
|
207
|
+
: { retryable: failureDetails.retryable }),
|
|
208
|
+
},
|
|
209
|
+
});
|
|
197
210
|
}
|
|
198
211
|
// Normalize connector failures; never surface backend internals or credentials.
|
|
199
|
-
return fail('connector_error', `connector failed for operation "${ref.operation}"
|
|
212
|
+
return fail('connector_error', `connector failed for operation "${ref.operation}"`, {
|
|
213
|
+
connector: {
|
|
214
|
+
connectorId: ref.connectorId,
|
|
215
|
+
connectorVersion: ref.connectorVersion,
|
|
216
|
+
operation: ref.operation,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
200
219
|
}
|
|
201
220
|
if (customerRoute !== undefined) {
|
|
202
221
|
const cloned = cloneRoutedOutput(output, customerRoute.baseUrl);
|
|
@@ -278,6 +297,16 @@ function hostCallErrorSnapshot(value) {
|
|
|
278
297
|
? hostCallErrors.get(value)
|
|
279
298
|
: undefined;
|
|
280
299
|
}
|
|
300
|
+
/** Upstream status class for retained attribution; the exact status stays out of analytics. */
|
|
301
|
+
function statusClass(status) {
|
|
302
|
+
if (status === undefined)
|
|
303
|
+
return undefined;
|
|
304
|
+
if (status >= 500 && status <= 599)
|
|
305
|
+
return '5xx';
|
|
306
|
+
if (status >= 400 && status <= 499)
|
|
307
|
+
return '4xx';
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
281
310
|
export function fail(code, message, pathOrDetails) {
|
|
282
311
|
if (typeof pathOrDetails === 'string') {
|
|
283
312
|
return { ok: false, error: { code, message, path: pathOrDetails } };
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import type { CredentialProfile } from '@noodle-borg/compiler';
|
|
2
|
+
import type { ConnectorFailureCategory } from './connector/types.js';
|
|
2
3
|
import type { CustomerRouteBinding } from './customer-routing.js';
|
|
3
4
|
/** Why a tool call could not be fulfilled. Protocol and embedded adapters map this internal contract. */
|
|
4
5
|
export type ExecutionErrorCode = 'shape_only_artifact' | 'unknown_tool' | 'unknown_resource' | 'unknown_prompt' | 'unsupported_fulfilment' | 'connector_unavailable' | 'connector_route_unavailable' | 'credential_unavailable' | 'signature_drift' | 'policy_denied' | 'policy_error' | 'dispatch_denied' | 'usage_limit_exceeded' | 'duplicate_execution_suppressed' | 'execution_admission_error' | 'execution_cancelled' | 'arg_invalid' | 'invalid_continuation' | 'invalid_elicitation_flow' | 'invalid_confirmation_flow' | 'expression_error' | 'circular_call_dependency' | 'connector_error' | 'output_invalid';
|
|
6
|
+
/**
|
|
7
|
+
* Safe connector failure attribution for operator analytics (issue #1309): platform identifiers and
|
|
8
|
+
* bounded classification only — never an upstream message, response body, credential, or exact URL.
|
|
9
|
+
* Adapters must keep this off every model-facing wire channel.
|
|
10
|
+
*/
|
|
11
|
+
export interface ConnectorFailureAttribution {
|
|
12
|
+
readonly connectorId: string;
|
|
13
|
+
readonly connectorVersion: string;
|
|
14
|
+
readonly operation: string;
|
|
15
|
+
readonly category?: ConnectorFailureCategory;
|
|
16
|
+
/** Upstream HTTP status class; the exact status is deliberately not retained. */
|
|
17
|
+
readonly statusClass?: '4xx' | '5xx';
|
|
18
|
+
readonly attempts?: number;
|
|
19
|
+
readonly retryable?: boolean;
|
|
20
|
+
}
|
|
5
21
|
/** A typed execution failure. `path` locates the offending field/expression when applicable and
|
|
6
22
|
* never carries a sensitive value. */
|
|
7
23
|
export interface ExecutionError {
|
|
@@ -10,6 +26,8 @@ export interface ExecutionError {
|
|
|
10
26
|
readonly path?: string;
|
|
11
27
|
/** Stable, allowlisted machine-readable detail. Never an upstream error message. */
|
|
12
28
|
readonly reason?: string;
|
|
29
|
+
/** Present on connector failures; analytics-only, never serialized to a client. */
|
|
30
|
+
readonly connector?: ConnectorFailureAttribution;
|
|
13
31
|
/** Safe monthly-allowance reset instant, present only for commercial usage denial. */
|
|
14
32
|
readonly resetAt?: string;
|
|
15
33
|
/** Safe remediation text intended for a developer or agent. */
|
|
@@ -8,7 +8,8 @@ export async function authorizeDeveloperGrant(identity, context) {
|
|
|
8
8
|
});
|
|
9
9
|
if (grant === undefined)
|
|
10
10
|
return false;
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -5,7 +5,7 @@ import { assistantSessionUsageRequestEvent, captureAssistantUsage, } from '@nood
|
|
|
5
5
|
import { assistantSessionResponseSchema } from '@noodle-borg/wire-contracts';
|
|
6
6
|
import { readJsonBody, sendJson } from '../http-util.js';
|
|
7
7
|
import { assistantBrowserConfiguration } from './assistant-configuration.js';
|
|
8
|
-
import { applyBrowserCors, now } from './assistant-route-http.js';
|
|
8
|
+
import { applyBrowserCors, assistantSessionEndpoints, now } from './assistant-route-http.js';
|
|
9
9
|
/**
|
|
10
10
|
* The transport half of anonymous session minting. Every decision — which refusal, in which order, and
|
|
11
11
|
* whether the surface may serve at all — lives in `@noodle-borg/assistant-gateway`; this file parses a
|
|
@@ -76,6 +76,7 @@ export async function handlePublicAssistantSession(req, res, deps) {
|
|
|
76
76
|
: 'operator',
|
|
77
77
|
origin: input.origin,
|
|
78
78
|
publicEmbedId: input.embed.embedId,
|
|
79
|
+
boundSurface: 'public',
|
|
79
80
|
caller: { subject: input.subject, identityKind: 'anonymous' },
|
|
80
81
|
...(configuration ? { configuration } : {}),
|
|
81
82
|
createdAt: current.toISOString(),
|
|
@@ -94,17 +95,10 @@ export async function handlePublicAssistantSession(req, res, deps) {
|
|
|
94
95
|
}
|
|
95
96
|
// Only here, with the origin proven against the live surface, may the page read the response.
|
|
96
97
|
applyBrowserCors(req, res, origin);
|
|
97
|
-
const base = deps.serviceBase(req);
|
|
98
98
|
const sessionBody = {
|
|
99
99
|
token: result.token,
|
|
100
100
|
expiresAt: result.expiresAt,
|
|
101
|
-
endpoints:
|
|
102
|
-
turns: `${base}/v1/assistant/turns`,
|
|
103
|
-
toolConfirmations: `${base}/v1/assistant/tool-confirmations`,
|
|
104
|
-
interactions: `${base}/v1/assistant/interactions`,
|
|
105
|
-
apps: `${base}/v1/assistant/apps`,
|
|
106
|
-
sandbox: `${base}/v1/assistant/sandbox`,
|
|
107
|
-
},
|
|
101
|
+
endpoints: assistantSessionEndpoints(deps.serviceBase(req), deps.sandboxBase?.(req)),
|
|
108
102
|
...(configuration ? { configuration } : {}),
|
|
109
103
|
};
|
|
110
104
|
// Parse rather than trust: every published @noodleseed/assistant widget consumes this shape, and the
|
|
@@ -47,4 +47,15 @@ export function applyBrowserCors(req, res, expectedOrigin) {
|
|
|
47
47
|
export function now(deps) {
|
|
48
48
|
return deps.clock?.() ?? new Date();
|
|
49
49
|
}
|
|
50
|
+
/** The one endpoint map every session mint returns; three mints, one source of truth. */
|
|
51
|
+
export function assistantSessionEndpoints(base, sandboxBase = base) {
|
|
52
|
+
return {
|
|
53
|
+
turns: `${base}/v1/assistant/turns`,
|
|
54
|
+
toolConfirmations: `${base}/v1/assistant/tool-confirmations`,
|
|
55
|
+
interactions: `${base}/v1/assistant/interactions`,
|
|
56
|
+
apps: `${base}/v1/assistant/apps`,
|
|
57
|
+
sandbox: `${sandboxBase}/v1/assistant/sandbox`,
|
|
58
|
+
transcript: `${base}/v1/assistant/transcript`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
50
61
|
//# sourceMappingURL=assistant-route-http.js.map
|
|
@@ -1,58 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* customer page's CSP, so a strict `script-src` silently blanks every widget. Serving this relay
|
|
5
|
-
* document from the service origin gives it — and the inner `srcdoc` widget document that inherits
|
|
6
|
-
* from it — a CSP the platform controls. The body is pinned byte-for-byte to
|
|
7
|
-
* `contract/v1/assistant-sandbox-document.html` and to the published client's srcdoc fallback; any
|
|
8
|
-
* relay-protocol change is a coordinated contract event, never a local edit.
|
|
9
|
-
*/
|
|
10
|
-
export const ASSISTANT_SANDBOX_DOCUMENT = `<!doctype html><meta charset="utf-8"><style>html,body,iframe{border:0;margin:0;width:100%;height:100%;overflow:hidden}body{background:transparent}</style><script>
|
|
11
|
-
let inner;
|
|
12
|
-
const ready={jsonrpc:'2.0',method:'ui/notifications/sandbox-proxy-ready',params:{}};
|
|
13
|
-
// Re-announce until the host acknowledges with the resource: when this document is hosted
|
|
14
|
-
// cross-origin, the first announcement can arrive before the host's load handler is listening.
|
|
15
|
-
const announce=setInterval(()=>parent.postMessage(ready,'*'),120);
|
|
16
|
-
setTimeout(()=>clearInterval(announce),15000);
|
|
17
|
-
addEventListener('message',(event)=>{
|
|
18
|
-
if(event.source===parent){
|
|
19
|
-
const message=event.data;
|
|
20
|
-
if(message?.method==='ui/notifications/sandbox-resource-ready'){
|
|
21
|
-
clearInterval(announce);
|
|
22
|
-
if(inner)return;
|
|
23
|
-
inner=document.createElement('iframe');
|
|
24
|
-
inner.setAttribute('sandbox',message.params?.sandbox||'allow-scripts');
|
|
25
|
-
inner.setAttribute('referrerpolicy','no-referrer');
|
|
26
|
-
inner.srcdoc=String(message.params?.html||'');
|
|
27
|
-
document.body.replaceChildren(inner);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
inner?.contentWindow?.postMessage(message,'*');
|
|
31
|
-
} else if(inner && event.source===inner.contentWindow) {
|
|
32
|
-
parent.postMessage(event.data,'*');
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
parent.postMessage(ready,'*');
|
|
36
|
-
</script>`;
|
|
37
|
-
/**
|
|
38
|
-
* The sandbox document's own CSP. `sandbox allow-scripts` forces an opaque origin server-side even
|
|
39
|
-
* if a future embedder frames the URL without a sandbox attribute (the document can never read
|
|
40
|
-
* service cookies or storage); inline script/style must be allowed because the relay script and the
|
|
41
|
-
* inner widget document's injected bridge are inline by design; everything else stays closed —
|
|
42
|
-
* widget data flows only over the postMessage bridge, never direct fetch.
|
|
43
|
-
*/
|
|
44
|
-
const SANDBOX_CSP = "sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src https: data: blob:; font-src https: data:; frame-src about:; form-action 'none'; base-uri 'none'; frame-ancestors *";
|
|
45
|
-
/** Serve the static, secret-free sandbox document. GET-only; cacheable despite the no-store baseline. */
|
|
46
|
-
export function handleAssistantSandbox(req, res) {
|
|
1
|
+
import { ASSISTANT_SANDBOX_DOCUMENT, resolveAssistantSandboxResponsePolicy, } from '@noodle-borg/assistant-gateway/portable';
|
|
2
|
+
/** Serve the static, secret-free sandbox relay and apply its validated CSP before writing the body. */
|
|
3
|
+
export function handleAssistantSandbox(req, res, options = {}) {
|
|
47
4
|
if (req.method !== 'GET') {
|
|
48
5
|
res.statusCode = 405;
|
|
49
6
|
res.setHeader('Allow', 'GET');
|
|
50
7
|
res.end();
|
|
51
8
|
return;
|
|
52
9
|
}
|
|
10
|
+
const policy = resolveAssistantSandboxResponsePolicy(req.url ?? '/', options.isolatedOrigin === true);
|
|
11
|
+
res.setHeader('Content-Security-Policy', policy.contentSecurityPolicy);
|
|
12
|
+
if (!policy.ok) {
|
|
13
|
+
res.statusCode = 400;
|
|
14
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
15
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
16
|
+
res.end('Invalid sandbox policy');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
53
19
|
res.statusCode = 200;
|
|
54
20
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
55
|
-
res.setHeader('Content-Security-Policy', SANDBOX_CSP);
|
|
56
21
|
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
57
22
|
res.end(ASSISTANT_SANDBOX_DOCUMENT);
|
|
58
23
|
}
|