@noodleseed/one 0.141.1 → 0.142.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/bin.js +0 -0
- package/dist/commands/assistant-appearance-ops.d.ts +3 -0
- package/dist/commands/assistant-appearance-ops.d.ts.map +1 -0
- package/dist/commands/assistant-appearance-ops.js +148 -0
- package/dist/commands/assistant-appearance-ops.js.map +1 -0
- package/dist/commands/assistant-ops.d.ts.map +1 -1
- package/dist/commands/assistant-ops.js +4 -1
- package/dist/commands/assistant-ops.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 +25 -18
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-appearance-store.d.ts +34 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-appearance-store.js +12 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-appearance.d.ts +18 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-appearance.js +228 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-configuration.d.ts +31 -0
- package/node_modules/@noodle-borg/{service/dist/routes → assistant-gateway/dist}/assistant-configuration.js +12 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-delegated-projection.d.ts +12 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.d.ts +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-store.js +1 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-view-availability.d.ts +38 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.d.ts +3 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/embed-store.js +4 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-assistant-appearance-store.d.ts +8 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/in-memory-assistant-appearance-store.js +26 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.d.ts +7 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/portable.js +7 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-configuration.d.ts +22 -0
- package/node_modules/@noodle-borg/assistant-gateway/dist/public-configuration.js +25 -0
- package/node_modules/@noodle-borg/assistant-gateway/package.json +3 -1
- package/node_modules/@noodle-borg/authoring/dist/assistant.d.ts +6 -2
- package/node_modules/@noodle-borg/authoring/dist/react/components.d.ts +3 -3
- package/node_modules/@noodle-borg/authoring/dist/react/native-components.d.ts +2 -2
- package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-assistant.js +38 -0
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.d.ts +56 -4
- package/node_modules/@noodle-borg/compiler/dist/manifest/schema.js +25 -47
- package/node_modules/@noodle-borg/service/dist/credential-broker.js +1 -1
- package/node_modules/@noodle-borg/service/dist/invocation-context.js +1 -2
- package/node_modules/@noodle-borg/service/dist/local-options.d.ts +1 -0
- package/node_modules/@noodle-borg/service/dist/registry-compile.js +1 -1
- package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +1 -3
- package/node_modules/@noodle-borg/service/dist/routes/assistant-appearance.js +104 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-dispatch.js +24 -4
- package/node_modules/@noodle-borg/service/dist/routes/assistant-interaction-replay.js +1 -2
- package/node_modules/@noodle-borg/service/dist/routes/assistant-interactions.js +1 -2
- package/node_modules/@noodle-borg/service/dist/routes/assistant-public-configuration.js +41 -0
- package/node_modules/@noodle-borg/service/dist/routes/assistant-public-session.js +7 -12
- package/node_modules/@noodle-borg/service/dist/routes/assistant-route-http.js +2 -2
- package/node_modules/@noodle-borg/service/dist/routes/assistant-sandbox.js +47 -12
- package/node_modules/@noodle-borg/service/dist/routes/assistant.js +4 -5
- package/node_modules/@noodle-borg/service/dist/service.js +3 -15
- package/node_modules/@noodleseed/assistant/README.md +63 -24
- package/node_modules/@noodleseed/assistant/package.json +1 -1
- package/package.json +1 -1
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-app-sandbox.d.ts +0 -13
- package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-app-sandbox.js +0 -204
- /package/node_modules/@noodle-borg/{service → assistant-gateway}/dist/assistant-delegated-projection.js +0 -0
- /package/node_modules/@noodle-borg/{service → assistant-gateway}/dist/assistant-view-availability.js +0 -0
|
@@ -1,12 +1,10 @@
|
|
|
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, authenticatedSurfaceOf, dispatchAssistantTool, projectAssistantGuide, publicSurfaceOf, selectAssistantModelTools, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
|
|
3
|
+
import { assistantModelToolOncePerSession, assistantModelToolRequiredWhenVisible, assistantViewAvailableData, authenticatedSurfaceOf, dispatchAssistantTool, projectAssistantGuide, publicSurfaceOf, selectAssistantModelTools, toolTouchesDelegatedAuth, 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';
|
|
7
7
|
import { executeTool } from '@noodle-borg/runtime';
|
|
8
|
-
import { toolTouchesDelegatedAuth } from '../assistant-delegated-projection.js';
|
|
9
|
-
import { assistantViewAvailableData } from '../assistant-view-availability.js';
|
|
10
8
|
import { invocationContextSystemMessages } from '../invocation-context.js';
|
|
11
9
|
import { interceptForElevation, offersSignIn } from './assistant-elevation.js';
|
|
12
10
|
import { assistantKnowledgeModelTools, executeAssistantKnowledgeSearch, findAssistantKnowledgeComponent, KNOWLEDGE_CITATION_GUIDANCE, resolveAssistantKnowledge, } from './assistant-knowledge.js';
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { assistantAppearanceOverrideSchema, assistantBrowserConfiguration, resolveAssistantAppearanceConfiguration, } from '@noodle-borg/assistant-gateway/portable';
|
|
2
|
+
import { readJsonBody, sendForbidden, sendJson } from '../http-util.js';
|
|
3
|
+
import { now } from './assistant-route-http.js';
|
|
4
|
+
import { authorizeControlPlane } from './control-plane.js';
|
|
5
|
+
export async function handleAssistantAppearance(req, res, tenant, deps) {
|
|
6
|
+
const store = deps.appearance;
|
|
7
|
+
if (store === undefined)
|
|
8
|
+
return sendJson(res, 503, { error: 'assistant appearance is unavailable' });
|
|
9
|
+
const identity = await authorizeControlPlane(req, res, deps.gate, { requireIdentity: false });
|
|
10
|
+
if (identity === false)
|
|
11
|
+
return;
|
|
12
|
+
if (identity && !identity.superAdmin) {
|
|
13
|
+
const member = await deps.controlPlane.isOrgMember({
|
|
14
|
+
org: tenant.org,
|
|
15
|
+
subject: identity.subject,
|
|
16
|
+
});
|
|
17
|
+
if (!member)
|
|
18
|
+
return sendForbidden(res, 'forbidden');
|
|
19
|
+
}
|
|
20
|
+
if (req.method === 'GET')
|
|
21
|
+
return respond(res, tenant, deps);
|
|
22
|
+
const expectedRevision = parseExpectedRevision(req.headers['if-match']);
|
|
23
|
+
if (expectedRevision === undefined) {
|
|
24
|
+
return sendJson(res, req.headers['if-match'] === undefined ? 428 : 400, {
|
|
25
|
+
error: req.headers['if-match'] === undefined ? 'If-Match is required' : 'invalid If-Match',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
let override;
|
|
29
|
+
if (req.method === 'PUT') {
|
|
30
|
+
const body = await readJsonBody(req, deps.maxBody);
|
|
31
|
+
if (!body.ok)
|
|
32
|
+
return sendJson(res, body.status, { error: body.error });
|
|
33
|
+
const parsed = assistantAppearanceOverrideSchema.safeParse(body.value);
|
|
34
|
+
if (!parsed.success)
|
|
35
|
+
return sendJson(res, 400, { error: 'invalid assistant appearance' });
|
|
36
|
+
override = parsed.data;
|
|
37
|
+
}
|
|
38
|
+
else if (req.method !== 'DELETE') {
|
|
39
|
+
return sendJson(res, 405, { error: 'method not allowed' });
|
|
40
|
+
}
|
|
41
|
+
const updated = await store.replace({
|
|
42
|
+
tenant,
|
|
43
|
+
expectedRevision,
|
|
44
|
+
override,
|
|
45
|
+
updatedAt: now(deps),
|
|
46
|
+
updatedBy: identity?.subject ?? 'local-operator',
|
|
47
|
+
});
|
|
48
|
+
if (!updated.ok) {
|
|
49
|
+
return sendJson(res, 409, {
|
|
50
|
+
error: 'assistant appearance revision conflict',
|
|
51
|
+
currentRevision: updated.currentRevision,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const target = await deps.registry.getActiveByTenant(tenant);
|
|
55
|
+
await deps.audit.emit({
|
|
56
|
+
eventType: updated.record.override === undefined
|
|
57
|
+
? 'assistant.appearance.reset'
|
|
58
|
+
: 'assistant.appearance.updated',
|
|
59
|
+
org: tenant.org,
|
|
60
|
+
app: tenant.app,
|
|
61
|
+
env: tenant.env,
|
|
62
|
+
...(target?.deploymentId ? { deploymentId: target.deploymentId } : {}),
|
|
63
|
+
decision: 'allow',
|
|
64
|
+
status: 200,
|
|
65
|
+
...(identity ? { actorSubject: identity.subject } : {}),
|
|
66
|
+
...(identity?.email ? { actorEmail: identity.email } : {}),
|
|
67
|
+
details: {
|
|
68
|
+
revision: updated.record.revision,
|
|
69
|
+
hasOverride: updated.record.override !== undefined,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
return respond(res, tenant, deps, updated.record, target);
|
|
73
|
+
}
|
|
74
|
+
async function respond(res, tenant, deps, knownRecord, knownTarget) {
|
|
75
|
+
const target = knownTarget ?? (await deps.registry.getActiveByTenant(tenant));
|
|
76
|
+
const developer = target
|
|
77
|
+
? assistantBrowserConfiguration(target.served.artifact.server)
|
|
78
|
+
: undefined;
|
|
79
|
+
const record = knownRecord ?? (await deps.appearance?.get(tenant));
|
|
80
|
+
const resolution = resolveAssistantAppearanceConfiguration(developer, record?.override);
|
|
81
|
+
const revision = record?.revision ?? 0;
|
|
82
|
+
res.setHeader('etag', `"${revision}"`);
|
|
83
|
+
return sendJson(res, 200, {
|
|
84
|
+
ok: true,
|
|
85
|
+
assistantEnabled: target?.served.artifact.server.assistant !== undefined,
|
|
86
|
+
revision,
|
|
87
|
+
fallback: resolution.fallback,
|
|
88
|
+
developer: developer ?? null,
|
|
89
|
+
override: record?.override ?? null,
|
|
90
|
+
effective: resolution.effective ?? null,
|
|
91
|
+
provenance: resolution.provenance,
|
|
92
|
+
...(record ? { updatedAt: record.updatedAt.toISOString(), updatedBy: record.updatedBy } : {}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function parseExpectedRevision(value) {
|
|
96
|
+
if (typeof value !== 'string')
|
|
97
|
+
return undefined;
|
|
98
|
+
const match = /^"(0|[1-9][0-9]*)"$/.exec(value);
|
|
99
|
+
if (!match?.[1])
|
|
100
|
+
return undefined;
|
|
101
|
+
const revision = Number(match[1]);
|
|
102
|
+
return Number.isSafeInteger(revision) ? revision : undefined;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=assistant-appearance.js.map
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { isAssistantSandboxHost, } from '@noodle-borg/assistant-gateway/portable';
|
|
2
1
|
import { handleAssistantAppRequest, handleAssistantPreflight, handleAssistantSession, handleAssistantTurn, handleConsoleApprovalNonce, } from './assistant.js';
|
|
2
|
+
import { handleAssistantAppearance } from './assistant-appearance.js';
|
|
3
3
|
import { handleAssistantClients } from './assistant-clients.js';
|
|
4
4
|
import { handleAssistantDoctor } from './assistant-doctor.js';
|
|
5
5
|
import { handleAssistantEmbedScript } from './assistant-embed-script.js';
|
|
6
6
|
import { handleAssistantEmbeds } from './assistant-embeds.js';
|
|
7
7
|
import { handleAssistantConfirmation, handleAssistantInteraction, } from './assistant-interactions.js';
|
|
8
|
+
import { handlePublicAssistantConfiguration } from './assistant-public-configuration.js';
|
|
8
9
|
import { handlePublicAssistantSession } from './assistant-public-session.js';
|
|
9
10
|
import { handleAssistantSandbox } from './assistant-sandbox.js';
|
|
10
11
|
import { handleAssistantTranscript } from './assistant-transcript.js';
|
|
@@ -36,14 +37,24 @@ export function dispatchAssistantRoutes(req, res, url, deps) {
|
|
|
36
37
|
deps.applySecurityHeaders(res, deps.tls);
|
|
37
38
|
if (deps.enforceHttps(req, res, deps.tls))
|
|
38
39
|
return true;
|
|
39
|
-
|
|
40
|
-
const isolatedOrigin = isAssistantSandboxHost(req.headers.host, sandboxBase);
|
|
41
|
-
handleAssistantSandbox(req, res, { isolatedOrigin });
|
|
40
|
+
handleAssistantSandbox(req, res);
|
|
42
41
|
return true;
|
|
43
42
|
}
|
|
44
43
|
if (url.pathname === '/v1/assistant/public-sessions' && req.method === 'POST') {
|
|
45
44
|
return run(req, res, deps, () => handlePublicAssistantSession(req, res, deps));
|
|
46
45
|
}
|
|
46
|
+
const publicConfigurationMatch = /^\/v1\/assistant\/public-configurations\/([^/]+)$/.exec(url.pathname);
|
|
47
|
+
if (publicConfigurationMatch) {
|
|
48
|
+
let embedId;
|
|
49
|
+
try {
|
|
50
|
+
embedId = decodeURIComponent(publicConfigurationMatch[1] ?? '');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
deps.sendJson(res, 400, { error: 'invalid embed id' });
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return run(req, res, deps, () => handlePublicAssistantConfiguration(req, res, embedId, deps));
|
|
57
|
+
}
|
|
47
58
|
if (url.pathname === '/v1/assistant/sessions' && req.method === 'POST') {
|
|
48
59
|
return run(req, res, deps, () => handleAssistantSession(req, res, deps));
|
|
49
60
|
}
|
|
@@ -57,6 +68,15 @@ export function dispatchAssistantRoutes(req, res, url, deps) {
|
|
|
57
68
|
return run(req, res, deps, () => handleAssistantTranscript(req, res, deps));
|
|
58
69
|
}
|
|
59
70
|
const doctorMatch = /^\/v1\/orgs\/([^/]+)\/apps\/([^/]+)\/envs\/([^/]+)\/assistant\/doctor$/.exec(url.pathname);
|
|
71
|
+
const appearanceMatch = /^\/v1\/orgs\/([^/]+)\/apps\/([^/]+)\/envs\/([^/]+)\/assistant\/appearance$/.exec(url.pathname);
|
|
72
|
+
if (appearanceMatch) {
|
|
73
|
+
const tenant = decodeTenant(appearanceMatch);
|
|
74
|
+
if (!tenant) {
|
|
75
|
+
deps.sendJson(res, 400, { error: 'invalid tenant path' });
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return run(req, res, deps, () => handleAssistantAppearance(req, res, tenant, deps));
|
|
79
|
+
}
|
|
60
80
|
if (doctorMatch) {
|
|
61
81
|
const tenant = decodeTenant(doctorMatch);
|
|
62
82
|
if (!tenant) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { assistantInteractionTool, auditAssistantInteraction, } from '@noodle-borg/assistant-gateway/portable';
|
|
2
|
-
import { assistantViewAvailableData } from '../assistant-view-availability.js';
|
|
1
|
+
import { assistantInteractionTool, assistantViewAvailableData, auditAssistantInteraction, } from '@noodle-borg/assistant-gateway/portable';
|
|
3
2
|
import { sendJson } from '../http-util.js';
|
|
4
3
|
import { now } from './assistant.js';
|
|
5
4
|
import { assistantInteractionScope } from './assistant-interaction-resolution.js';
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { AssistantInteractionCapacityError, assistantConfirmationReview, assistantConfirmationTransition, assistantInputTransition, assistantInteractionTool, assistantPreparedArgumentReview, assistantPreparedToolContinuation, assistantSafeOutput, assistantToolContinuation, auditAssistantInteraction, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
|
|
1
|
+
import { AssistantInteractionCapacityError, assistantConfirmationReview, assistantConfirmationTransition, assistantInputTransition, assistantInteractionTool, assistantPreparedArgumentReview, assistantPreparedToolContinuation, assistantSafeOutput, assistantToolContinuation, assistantViewAvailableData, auditAssistantInteraction, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
|
|
2
2
|
import { executePreparedTool, executeToolInteractive, resumeTool, resumeToolPreparation, } from '@noodle-borg/runtime';
|
|
3
|
-
import { assistantViewAvailableData } from '../assistant-view-availability.js';
|
|
4
3
|
import { readJsonBody, sendJson } from '../http-util.js';
|
|
5
4
|
import { applyBrowserCors, authenticateSession, now, } from './assistant.js';
|
|
6
5
|
import { replayOrConflict, writeUnknownOutcome } from './assistant-interaction-replay.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { authorizePublicConfiguration, effectiveAssistantBrowserConfiguration, publicEmbedTenant, publicSurfaceOf, } from '@noodle-borg/assistant-gateway/portable';
|
|
2
|
+
import { sendJson } from '../http-util.js';
|
|
3
|
+
import { applyBrowserCors } from './assistant-route-http.js';
|
|
4
|
+
/** Browser-safe first paint for a public launcher; never mints a session or spends admission. */
|
|
5
|
+
export async function handlePublicAssistantConfiguration(req, res, embedId, deps) {
|
|
6
|
+
if (req.method !== 'GET')
|
|
7
|
+
return sendJson(res, 405, { error: 'method not allowed' });
|
|
8
|
+
if (deps.publicEmbeds === undefined) {
|
|
9
|
+
return sendJson(res, 503, { error: 'public assistant surfaces are not enabled' });
|
|
10
|
+
}
|
|
11
|
+
const origin = typeof req.headers.origin === 'string' ? req.headers.origin : undefined;
|
|
12
|
+
const state = {};
|
|
13
|
+
const authorized = await authorizePublicConfiguration({ embedId, origin }, {
|
|
14
|
+
embeds: deps.publicEmbeds,
|
|
15
|
+
resolveActiveSurface: async (embed) => {
|
|
16
|
+
state.target = await deps.registry.getActiveByTenant(publicEmbedTenant(embed));
|
|
17
|
+
return state.target
|
|
18
|
+
? publicSurfaceOf(state.target.served.artifact.server.assistant)
|
|
19
|
+
: undefined;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if (!authorized.ok) {
|
|
23
|
+
return sendJson(res, authorized.status, {
|
|
24
|
+
error: authorized.message,
|
|
25
|
+
code: authorized.code,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const target = state.target;
|
|
29
|
+
if (target === undefined) {
|
|
30
|
+
return sendJson(res, 409, { error: 'assistant deployment is unavailable' });
|
|
31
|
+
}
|
|
32
|
+
const resolved = await effectiveAssistantBrowserConfiguration(target.served.artifact.server, publicEmbedTenant(authorized.embed), deps.appearance);
|
|
33
|
+
applyBrowserCors(req, res, origin);
|
|
34
|
+
res.setHeader('cache-control', 'private, no-cache');
|
|
35
|
+
return sendJson(res, 200, {
|
|
36
|
+
...(resolved.effective ? { configuration: resolved.effective } : {}),
|
|
37
|
+
fallback: resolved.fallback,
|
|
38
|
+
revision: resolved.record?.revision ?? 0,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=assistant-public-configuration.js.map
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { ADMISSION_DEFAULTS, clientAddressBucket } from '@noodle-borg/admission-limits/portable';
|
|
3
|
-
import { mintPublicSession, publicSurfaceOf, } from '@noodle-borg/assistant-gateway/portable';
|
|
3
|
+
import { effectiveAssistantBrowserConfiguration, mintPublicSession, publicEmbedTenant, publicSurfaceOf, } from '@noodle-borg/assistant-gateway/portable';
|
|
4
4
|
import { assistantSessionUsageRequestEvent, captureAssistantUsage, } from '@noodle-borg/observability';
|
|
5
5
|
import { assistantSessionResponseSchema } from '@noodle-borg/wire-contracts';
|
|
6
6
|
import { readJsonBody, sendJson } from '../http-util.js';
|
|
7
|
-
import { assistantBrowserConfiguration } from './assistant-configuration.js';
|
|
8
7
|
import { applyBrowserCors, assistantSessionEndpoints, now } from './assistant-route-http.js';
|
|
9
8
|
/**
|
|
10
9
|
* The transport half of anonymous session minting. Every decision — which refusal, in which order, and
|
|
@@ -16,10 +15,6 @@ import { applyBrowserCors, assistantSessionEndpoints, now } from './assistant-ro
|
|
|
16
15
|
* echoes the validated origin, and a refusal echoes nothing, so a page that is not on the allowlist
|
|
17
16
|
* fails visibly instead of quietly receiving a usable token.
|
|
18
17
|
*/
|
|
19
|
-
/** The embed record stores the tenant flat; the registry and session store want it shaped. */
|
|
20
|
-
function tenantOf(embed) {
|
|
21
|
-
return { org: embed.org, app: embed.app, env: embed.env };
|
|
22
|
-
}
|
|
23
18
|
export async function handlePublicAssistantSession(req, res, deps) {
|
|
24
19
|
const usageStartedAt = performance.now();
|
|
25
20
|
const embeds = deps.publicEmbeds;
|
|
@@ -45,12 +40,12 @@ export async function handlePublicAssistantSession(req, res, deps) {
|
|
|
45
40
|
embeds,
|
|
46
41
|
counters,
|
|
47
42
|
resolveActiveSurface: async (embed) => {
|
|
48
|
-
const target = await deps.registry.getActiveByTenant(
|
|
43
|
+
const target = await deps.registry.getActiveByTenant(publicEmbedTenant(embed));
|
|
49
44
|
if (!target?.deploymentId)
|
|
50
45
|
return undefined;
|
|
51
46
|
if (target.served.artifact.server.assistant?.model.kind === 'noodle-managed') {
|
|
52
47
|
sponsoredBudget = (await deps.managedModelResolver?.resolve({
|
|
53
|
-
tenant:
|
|
48
|
+
tenant: publicEmbedTenant(embed),
|
|
54
49
|
deploymentId: target.deploymentId,
|
|
55
50
|
}))?.publicAdmission;
|
|
56
51
|
}
|
|
@@ -60,16 +55,16 @@ export async function handlePublicAssistantSession(req, res, deps) {
|
|
|
60
55
|
},
|
|
61
56
|
resolveBudgetBounds: async () => sponsoredBudget,
|
|
62
57
|
createSession: async (input) => {
|
|
63
|
-
const target = await deps.registry.getActiveByTenant(
|
|
58
|
+
const target = await deps.registry.getActiveByTenant(publicEmbedTenant(input.embed));
|
|
64
59
|
if (!target?.deploymentId)
|
|
65
60
|
throw new Error('assistant deployment vanished mid-mint');
|
|
66
61
|
const current = now(deps);
|
|
67
|
-
configuration =
|
|
62
|
+
configuration = (await effectiveAssistantBrowserConfiguration(target.served.artifact.server, publicEmbedTenant(input.embed), deps.appearance)).effective;
|
|
68
63
|
const created = await deps.store.createSession({
|
|
69
64
|
// A public surface has no client credential, so the embed id stands in as the minting
|
|
70
65
|
// authority. It is not a secret and nothing downstream may begin treating it as one.
|
|
71
66
|
clientId: input.embed.embedId,
|
|
72
|
-
tenant:
|
|
67
|
+
tenant: publicEmbedTenant(input.embed),
|
|
73
68
|
deploymentId: target.deploymentId,
|
|
74
69
|
modelSource: target.served.artifact.server.assistant?.model.kind === 'noodle-managed'
|
|
75
70
|
? 'noodle-managed'
|
|
@@ -98,7 +93,7 @@ export async function handlePublicAssistantSession(req, res, deps) {
|
|
|
98
93
|
const sessionBody = {
|
|
99
94
|
token: result.token,
|
|
100
95
|
expiresAt: result.expiresAt,
|
|
101
|
-
endpoints: assistantSessionEndpoints(deps.serviceBase(req)
|
|
96
|
+
endpoints: assistantSessionEndpoints(deps.serviceBase(req)),
|
|
102
97
|
...(configuration ? { configuration } : {}),
|
|
103
98
|
};
|
|
104
99
|
// Parse rather than trust: every published @noodleseed/assistant widget consumes this shape, and the
|
|
@@ -48,13 +48,13 @@ export function now(deps) {
|
|
|
48
48
|
return deps.clock?.() ?? new Date();
|
|
49
49
|
}
|
|
50
50
|
/** The one endpoint map every session mint returns; three mints, one source of truth. */
|
|
51
|
-
export function assistantSessionEndpoints(base
|
|
51
|
+
export function assistantSessionEndpoints(base) {
|
|
52
52
|
return {
|
|
53
53
|
turns: `${base}/v1/assistant/turns`,
|
|
54
54
|
toolConfirmations: `${base}/v1/assistant/tool-confirmations`,
|
|
55
55
|
interactions: `${base}/v1/assistant/interactions`,
|
|
56
56
|
apps: `${base}/v1/assistant/apps`,
|
|
57
|
-
sandbox: `${
|
|
57
|
+
sandbox: `${base}/v1/assistant/sandbox`,
|
|
58
58
|
transcript: `${base}/v1/assistant/transcript`,
|
|
59
59
|
};
|
|
60
60
|
}
|
|
@@ -1,23 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Hosted widget sandbox document (ADR 0151 addendum). The embedded assistant renders MCP App
|
|
3
|
+
* widgets in a double iframe; when the outer frame is an `about:srcdoc` document it inherits the
|
|
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) {
|
|
4
47
|
if (req.method !== 'GET') {
|
|
5
48
|
res.statusCode = 405;
|
|
6
49
|
res.setHeader('Allow', 'GET');
|
|
7
50
|
res.end();
|
|
8
51
|
return;
|
|
9
52
|
}
|
|
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
|
-
}
|
|
19
53
|
res.statusCode = 200;
|
|
20
54
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
55
|
+
res.setHeader('Content-Security-Policy', SANDBOX_CSP);
|
|
21
56
|
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
22
57
|
res.end(ASSISTANT_SANDBOX_DOCUMENT);
|
|
23
58
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { clientAddressBucket, } from '@noodle-borg/admission-limits/portable';
|
|
2
|
-
import { ASSISTANT_SESSION_IDLE_MS, dispatchAssistantTool, parseAssistantContextPreferences, parseAssistantCustomerRouting, resumeTurnMessage, resumeUnavailableMessage, shouldAutoResume, surfaceBindingForOrigin, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
|
|
2
|
+
import { ASSISTANT_SESSION_IDLE_MS, dispatchAssistantTool, effectiveAssistantBrowserConfiguration, parseAssistantContextPreferences, parseAssistantCustomerRouting, resumeTurnMessage, resumeUnavailableMessage, shouldAutoResume, surfaceBindingForOrigin, withAssistantSessionExecutionAuthority, } from '@noodle-borg/assistant-gateway/portable';
|
|
3
3
|
import { canonicalizeAuthorizationClaimValues } from '@noodle-borg/auth';
|
|
4
4
|
import { validateJsonSchemaWithDefaults } from '@noodle-borg/compiler';
|
|
5
5
|
import { tenantMcpUrl } from '@noodle-borg/module';
|
|
@@ -10,7 +10,6 @@ import { assistantMessageTurnRequestSchema, assistantResumeTurnRequestSchema, as
|
|
|
10
10
|
import { readJsonBody, sendForbidden, sendJson, sendUnauthorized } from '../http-util.js';
|
|
11
11
|
import { resolveInvocationContextSnapshot } from '../invocation-context.js';
|
|
12
12
|
import { createAssistantTurnStats, runAgentTurn } from './assistant-agent.js';
|
|
13
|
-
import { assistantBrowserConfiguration } from './assistant-configuration.js';
|
|
14
13
|
import { elevateAssistantSession } from './assistant-elevation.js';
|
|
15
14
|
import { executeAssistantKnowledgeSearch, findAssistantKnowledgeComponent, resolveAssistantKnowledge, } from './assistant-knowledge.js';
|
|
16
15
|
import { refusePublicTurn } from './assistant-public-turn.js';
|
|
@@ -100,7 +99,7 @@ export async function handleAssistantSession(req, res, deps) {
|
|
|
100
99
|
const declaredClaims = assistant.sessionClaims ?? {};
|
|
101
100
|
const claims = Object.fromEntries(Object.entries(isVerifiedClaims(parsed.claims) ? parsed.claims : {}).filter(([key]) => key in declaredClaims));
|
|
102
101
|
const current = now(deps);
|
|
103
|
-
const configuration =
|
|
102
|
+
const configuration = (await effectiveAssistantBrowserConfiguration(target.served.artifact.server, client.tenant, deps.appearance)).effective;
|
|
104
103
|
const roles = canonicalizeAuthorizationClaimValues(parsed.user.roles, 'role');
|
|
105
104
|
const scopes = canonicalizeAuthorizationClaimValues(parsed.user.scopes, 'scope');
|
|
106
105
|
// Hoisted so an elevation and a fresh exchange bind the *same* principal. Two constructions would be
|
|
@@ -142,7 +141,7 @@ export async function handleAssistantSession(req, res, deps) {
|
|
|
142
141
|
: {}),
|
|
143
142
|
caller,
|
|
144
143
|
configuration,
|
|
145
|
-
endpoints: assistantSessionEndpoints(deps.serviceBase(req)
|
|
144
|
+
endpoints: assistantSessionEndpoints(deps.serviceBase(req)),
|
|
146
145
|
});
|
|
147
146
|
}
|
|
148
147
|
const session = await deps.store.createSession({
|
|
@@ -166,7 +165,7 @@ export async function handleAssistantSession(req, res, deps) {
|
|
|
166
165
|
const sessionBody = {
|
|
167
166
|
token: session.token,
|
|
168
167
|
expiresAt: session.session.expiresAt,
|
|
169
|
-
endpoints: assistantSessionEndpoints(deps.serviceBase(req)
|
|
168
|
+
endpoints: assistantSessionEndpoints(deps.serviceBase(req)),
|
|
170
169
|
...(configuration ? { configuration } : {}),
|
|
171
170
|
};
|
|
172
171
|
// Every published @noodleseed/assistant widget consumes this shape; parse (don't just test)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { InMemoryAssistantAppearanceSettingsStore, InMemoryAssistantStore, } from '@noodle-borg/assistant-gateway/portable';
|
|
2
2
|
import { allowAllGate, InMemoryControlPlaneStore } from '@noodle-borg/control-plane/portable';
|
|
3
3
|
import { dispatchKnowledgeRequest } from '@noodle-borg/knowledge-operations/portable';
|
|
4
4
|
import { OPENAI_APPS_CHALLENGE_PATH } from '@noodle-borg/module';
|
|
@@ -50,9 +50,6 @@ const DEFAULT_MAX_BODY = 1 << 20;
|
|
|
50
50
|
const DEFAULT_MAX_DEPLOY_BODY = 32 * 1024 * 1024;
|
|
51
51
|
/** The combined service listener: tenant-scoped deploy management plus the multi-tenant MCP router. */
|
|
52
52
|
export function createServiceHandler(registry, options = {}) {
|
|
53
|
-
const assistantSandboxBaseUrl = options.assistantSandboxBaseUrl === undefined
|
|
54
|
-
? undefined
|
|
55
|
-
: validateAssistantSandboxBaseUrl(options.assistantSandboxBaseUrl, options.publicBaseUrl);
|
|
56
53
|
const logger = options.logger ?? noopLogger;
|
|
57
54
|
const tls = options.tls ?? {};
|
|
58
55
|
const buildInfo = options.buildInfo ?? resolveBuildInfo();
|
|
@@ -62,6 +59,7 @@ export function createServiceHandler(registry, options = {}) {
|
|
|
62
59
|
const alertRuleStore = options.alertRuleStore ?? new InMemoryAlertRuleStore();
|
|
63
60
|
const [intentSettings, intentEvents, requestEvents, intentPreviewOrgs] = createObservabilityStores(options);
|
|
64
61
|
const assistantStore = options.assistantStore ?? new InMemoryAssistantStore();
|
|
62
|
+
const assistantAppearance = options.assistantAppearance ?? new InMemoryAssistantAppearanceSettingsStore();
|
|
65
63
|
const anonymousLimiter = new AnonymousConsumerLimiter();
|
|
66
64
|
registry.setPlatformConnectors({ catalog: [BUILTIN_STATE_CATALOG_CONNECTOR], connectors: [] });
|
|
67
65
|
const moduleProviders = createServiceModuleProviders({
|
|
@@ -158,14 +156,6 @@ export function createServiceHandler(registry, options = {}) {
|
|
|
158
156
|
};
|
|
159
157
|
return (req, res) => {
|
|
160
158
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
161
|
-
const onAssistantSandboxHost = isAssistantSandboxHost(req.headers.host, assistantSandboxBaseUrl);
|
|
162
|
-
if (onAssistantSandboxHost && url.pathname !== '/v1/assistant/sandbox') {
|
|
163
|
-
applySecurityHeaders(res, tls);
|
|
164
|
-
if (enforceHttps(req, res, tls))
|
|
165
|
-
return;
|
|
166
|
-
sendJson(res, 404, { error: 'not found' });
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
159
|
// Liveness/readiness probes: un-gated and **not** HTTPS-enforced — Cloud Run's internal probe is plain
|
|
170
160
|
// HTTP, so enforcing HTTPS here would `426` the probe and the revision would never go healthy (ADR 0034).
|
|
171
161
|
// `/healthz` is a pure liveness 200 (no store touch); `/readyz` reflects the injected readiness probe.
|
|
@@ -248,6 +238,7 @@ export function createServiceHandler(registry, options = {}) {
|
|
|
248
238
|
if (dispatchAssistantRoutes(req, res, url, {
|
|
249
239
|
registry,
|
|
250
240
|
store: assistantStore,
|
|
241
|
+
appearance: assistantAppearance,
|
|
251
242
|
...(options.publicEmbeds !== undefined ? { publicEmbeds: options.publicEmbeds } : {}),
|
|
252
243
|
...(options.elevations !== undefined ? { elevations: options.elevations } : {}),
|
|
253
244
|
...(options.admissionCounters !== undefined
|
|
@@ -261,9 +252,6 @@ export function createServiceHandler(registry, options = {}) {
|
|
|
261
252
|
audit: activeAudit,
|
|
262
253
|
maxBody,
|
|
263
254
|
serviceBase: (request) => options.publicBaseUrl ?? baseFromRequest(request, tls),
|
|
264
|
-
...(assistantSandboxBaseUrl === undefined
|
|
265
|
-
? {}
|
|
266
|
-
: { sandboxBase: () => assistantSandboxBaseUrl }),
|
|
267
255
|
...(options.assistantModelFetch !== undefined
|
|
268
256
|
? { modelFetch: options.assistantModelFetch }
|
|
269
257
|
: {}),
|