From 1aceabee4498ec81fb3ebd62e1258e3fd6b40e72 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 25 Jun 2026 10:13:39 +0300 Subject: [PATCH] fix: restrict AI Workspace denial text to explicit denies --- .../skills/ai-workspace-context/SKILL.md | 2 +- .../assets/skills/engine-context/SKILL.md | 2 +- .../assets/skills/launcher-context/SKILL.md | 2 +- .../assets/skills/ops-context/SKILL.md | 2 +- .../src/scripts/smoke-run-profile.mjs | 28 ++++++++++++++----- .../ai-workspace-assistant/src/server.mjs | 22 ++++++++++----- 6 files changed, 40 insertions(+), 18 deletions(-) diff --git a/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/ai-workspace-context/SKILL.md b/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/ai-workspace-context/SKILL.md index 3a26700..1d53c3a 100644 --- a/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/ai-workspace-context/SKILL.md +++ b/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/ai-workspace-context/SKILL.md @@ -16,6 +16,6 @@ Core boundaries: Before acting, infer the target app from the user's intent and the current UI context. Prefer the run profile app catalog and available MCP/action namespaces over guessing from names. -If a requested app or capability is not present in the run profile, do not bypass it through another app. Say: `Доступ к модулю ограничен, обратитесь к администратору системы.` If another part of the request is available, complete only that available part and clearly separate the blocked part. +If a requested app or capability is explicitly marked `status: denied` in the current run profile, do not bypass it through another app. Say: `Доступ к модулю ограничен, обратитесь к администратору системы.` If another part of the request is available, complete only that available part and clearly separate the blocked part. Do not say this denial text for granted apps, available tool packs, or ordinary explanations. Never print MCP tokens, headers, setup codes, pairing codes, gateway tokens, or run-scoped secrets. diff --git a/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/engine-context/SKILL.md b/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/engine-context/SKILL.md index fcc839d..aebd33e 100644 --- a/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/engine-context/SKILL.md +++ b/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/engine-context/SKILL.md @@ -14,4 +14,4 @@ Routing rules: For write actions, prefer the structured Engine capability exposed in the current run profile. If the action is risky, present a preview and wait for confirmation when the run profile or action policy requires it. -If Engine is not available in the current run profile, do not simulate Engine writes with filesystem edits or unrelated tools. Say: `Доступ к модулю ограничен, обратитесь к администратору системы.` +If Engine is explicitly marked `status: denied` in the current run profile, do not simulate Engine writes with filesystem edits or unrelated tools. Say: `Доступ к модулю ограничен, обратитесь к администратору системы.` Do not say this denial text for ordinary Engine explanations, granted Engine access, or NDC Agent Core access. diff --git a/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/launcher-context/SKILL.md b/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/launcher-context/SKILL.md index d4a003c..a8ead2b 100644 --- a/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/launcher-context/SKILL.md +++ b/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/launcher-context/SKILL.md @@ -17,4 +17,4 @@ Do not treat AI Workspace Hub as Launcher. Hub only transports bridge traffic an Do not delete users, workspaces, projects, or role records through the assistant. Prefer safe actions such as block, disable, revoke access, or create an audit note when those actions are explicitly available. -If Launcher capability is unavailable in the current run profile, say: `Доступ к модулю ограничен, обратитесь к администратору системы.` +If Launcher capability is explicitly marked `status: denied` in the current run profile, say: `Доступ к модулю ограничен, обратитесь к администратору системы.` Do not say this denial text for granted Launcher access, available access actions, or ordinary explanations. diff --git a/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/ops-context/SKILL.md b/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/ops-context/SKILL.md index 834d1f8..3e802f2 100644 --- a/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/ops-context/SKILL.md +++ b/services/ai-workspace-assistant/src/assets/ai-workspace-npm/assets/skills/ops-context/SKILL.md @@ -15,4 +15,4 @@ Card-writing rules: - Put short verifiable work items into checker blocks. - Do not delete or archive cards unless a safe, explicitly exposed capability and confirmation policy allow the requested operation. Prefer comments or status updates. -If Ops is not available in the current run profile, say: `Доступ к модулю ограничен, обратитесь к администратору системы.` +If Ops is explicitly marked `status: denied` in the current run profile, say: `Доступ к модулю ограничен, обратитесь к администратору системы.` Do not say this denial text for granted Ops access or ordinary Ops explanations. diff --git a/services/ai-workspace-assistant/src/scripts/smoke-run-profile.mjs b/services/ai-workspace-assistant/src/scripts/smoke-run-profile.mjs index e0262a4..1ddecf4 100644 --- a/services/ai-workspace-assistant/src/scripts/smoke-run-profile.mjs +++ b/services/ai-workspace-assistant/src/scripts/smoke-run-profile.mjs @@ -97,6 +97,11 @@ const deniedByLauncher = mergeRunAppGrants(appGrants, normalizeEntitlementAdapte }, }, launcherAdapter)); const deniedMcpServers = runProfileMcpServersFromAppGrants({}, deniedByLauncher); +const explicitDeniedCatalog = buildRunProfileAppCatalog({ + appGrants: deniedByLauncher, + mcpServers: deniedMcpServers, + assistantActions, +}); const runProfile = { schemaVersion: "ai-workspace.run-profile.v1", runId: randomUUID(), @@ -141,6 +146,7 @@ const runProfile = { appCatalogIds: appCatalog.map((app) => app.appId).sort(), grantedAppIds: appAccess.grantedAppIds, deniedAppIds: appAccess.deniedAppIds, + notGrantedAppIds: appAccess.notGrantedAppIds, }, }; runProfile.diagnostics.profileHash = runProfileHash(runProfile); @@ -172,17 +178,23 @@ assert.equal(publicProfile.toolProfile.assistantActions.gatewayUrl, "https://ai- assert.equal(publicProfile.toolProfile.assistantActions.gatewayToken, ""); assert.deepEqual(publicProfile.toolProfile.assistantActions.actionIds, ["hub.access_request.list_pending", "hub.user.read_admin_summary"]); assert.equal(runProfile.appCatalog.find((app) => app.appId === "ops")?.status, "granted"); +assert.equal(runProfile.appCatalog.find((app) => app.appId === "ops")?.denied, false); assert.deepEqual(runProfile.appCatalog.find((app) => app.appId === "ops")?.mcpServerNames, ["nodedc_ops_agent"]); assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.status, "not-granted"); -assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.deniedText, ACCESS_DENIED_TEXT); +assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.denied, false); +assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.notGranted, true); +assert.equal(runProfile.appCatalog.find((app) => app.appId === "launcher")?.deniedText, null); assert.equal(launcherGrants.launcher.appId, "launcher"); assert.equal(launcherGrants.hub, undefined); assert.deepEqual(catalogWithLauncherGrant.find((app) => app.appId === "launcher")?.actionIds, ["hub.access_request.list_pending", "hub.user.read_admin_summary"]); assert.equal(deniedByLauncher.ops.status, "denied"); assert.deepEqual(deniedByLauncher.ops.mcpServers, []); assert.deepEqual(deniedMcpServers, []); +assert.equal(explicitDeniedCatalog.find((app) => app.appId === "ops")?.denied, true); +assert.equal(explicitDeniedCatalog.find((app) => app.appId === "ops")?.deniedText, ACCESS_DENIED_TEXT); assert.deepEqual(runProfile.appAccess.grantedAppIds, ["ops"]); -assert.deepEqual(runProfile.appAccess.deniedAppIds, ["engine", "launcher"]); +assert.deepEqual(runProfile.appAccess.deniedAppIds, []); +assert.deepEqual(runProfile.appAccess.notGrantedAppIds, ["engine", "launcher"]); assert.equal(JSON.stringify(publicProfile).includes(SECRET_TOKEN), false); assert.match(runProfile.diagnostics.profileHash, /^[a-f0-9]{16}$/); @@ -195,7 +207,8 @@ console.log(JSON.stringify({ "app_routing_catalog_in_run_profile", "hub_actions_route_to_launcher", "launcher_denial_removes_ops_mcp", - "denied_apps_have_standard_text", + "only_explicit_denied_apps_have_standard_text", + "not_granted_apps_do_not_claim_denied_access", "public_run_profile_redacts_mcp_headers", "public_run_profile_redacts_assistant_action_gateway_token", "stable_public_profile_hash", @@ -366,8 +379,9 @@ function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions }) skillId: entry.skillId, status, granted: status === "granted", - denied: status !== "granted", - deniedText: status !== "granted" ? entry.deniedText : null, + denied: status === "denied", + notGranted: status === "not-granted", + deniedText: status === "denied" ? entry.deniedText : null, actionIds: actionIds.filter((actionId) => actionIdPrefixesForEntry(entry).some((prefix) => String(actionId || "").startsWith(prefix))), mcpServerNames: status === "granted" ? uniqueStrings([ @@ -393,9 +407,9 @@ function summarizeRunAppAccess(appCatalog) { return { schemaVersion: "ai-workspace.app-access.v1", grantedAppIds: apps.filter((app) => app?.granted === true).map((app) => app.appId).sort(), - deniedAppIds: apps.filter((app) => app?.granted !== true).map((app) => app.appId).sort(), + deniedAppIds: apps.filter((app) => app?.status === "denied" || app?.denied === true).map((app) => app.appId).sort(), + notGrantedAppIds: apps.filter((app) => app?.status === "not-granted").map((app) => app.appId).sort(), availableSkillIds: apps.filter((app) => app?.granted === true).map((app) => app.skillId).filter(Boolean).sort(), - deniedText: ACCESS_DENIED_TEXT, }; } diff --git a/services/ai-workspace-assistant/src/server.mjs b/services/ai-workspace-assistant/src/server.mjs index ee07849..adf7db3 100644 --- a/services/ai-workspace-assistant/src/server.mjs +++ b/services/ai-workspace-assistant/src/server.mjs @@ -2101,6 +2101,7 @@ async function buildRunProfile({ owner, thread, executor, ownerSettings, bridgeP appCatalogIds: appCatalog.map((app) => app.appId).sort(), grantedAppIds: appAccess.grantedAppIds, deniedAppIds: appAccess.deniedAppIds, + notGrantedAppIds: appAccess.notGrantedAppIds, entitlementAdapters: grantResolution.diagnostics, mcpServerNames, requiredMcpServerNames, @@ -2611,11 +2612,12 @@ function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions }) skillId: entry.skillId, status, granted: status === "granted", - denied: status !== "granted", - deniedReason: status !== "granted" + denied: status === "denied", + notGranted: status === "not-granted", + deniedReason: status === "denied" ? optionalString(grant?.reason || grant?.deniedReason || grant?.denied_reason || grant?.status) || status : null, - deniedText: status !== "granted" + deniedText: status === "denied" ? optionalString(grant?.deniedText || grant?.denied_text) || entry.deniedText || ACCESS_DENIED_TEXT : null, whenToUse: uniqueStrings(entry.whenToUse), @@ -2633,9 +2635,9 @@ function summarizeRunAppAccess(appCatalog) { return { schemaVersion: "ai-workspace.app-access.v1", grantedAppIds: apps.filter((app) => app?.granted === true).map((app) => app.appId).sort(), - deniedAppIds: apps.filter((app) => app?.granted !== true).map((app) => app.appId).sort(), + deniedAppIds: apps.filter((app) => app?.status === "denied" || app?.denied === true).map((app) => app.appId).sort(), + notGrantedAppIds: apps.filter((app) => app?.status === "not-granted").map((app) => app.appId).sort(), availableSkillIds: apps.filter((app) => app?.granted === true).map((app) => app.skillId).filter(Boolean).sort(), - deniedText: ACCESS_DENIED_TEXT, }; } @@ -2659,16 +2661,22 @@ function buildRunProfilePolicyPrompt({ context, diagnostics, assistantActions }) `- entitlement source: ${diagnostics.entitlementAdapters?.source || "settings"}`, `- enabled tool packs: ${diagnostics.enabledToolPacks.length ? diagnostics.enabledToolPacks.join(", ") : "none"}`, `- app routes granted: ${diagnostics.grantedAppIds.length ? diagnostics.grantedAppIds.join(", ") : "none"}`, - `- app routes denied/not granted: ${diagnostics.deniedAppIds.length ? diagnostics.deniedAppIds.join(", ") : "none"}`, `- MCP servers available in this run: ${diagnostics.mcpServerNames.length ? diagnostics.mcpServerNames.join(", ") : "none"}`, `- assistant action ids available: ${Array.isArray(assistantActions?.actionIds) ? assistantActions.actionIds.join(", ") : "none"}`, "- Interpret the user's natural-language request first; call assistant actions only after selecting a structured action id.", "- Read assistant actions may execute after structured action selection. Privileged/write assistant actions require preview, explicit user confirmation, then execute.", "- Ops card actions advertised in this run are valid assistant actions: use ops.card.list_recent for reading cards, ops.card.create for creating cards, and ops.card.add_comment for comments instead of refusing because direct Ops MCP tools are absent.", "- Destructive assistant actions are forbidden; offer safe alternatives such as block/disable instead of delete.", - `- If the user requests a denied or not-granted app capability, do not bypass it. Say exactly: ${ACCESS_DENIED_TEXT}`, "- MCP tokens and headers are runtime secrets and must never be printed in public answers.", ]; + if (diagnostics.deniedAppIds.length) { + lines.push( + `- App routes explicitly denied by access control: ${diagnostics.deniedAppIds.join(", ")}.`, + `- If the user requests one of those explicitly denied app capabilities, do not bypass it. Say exactly: ${ACCESS_DENIED_TEXT}`, + ); + } else { + lines.push("- No app route is explicitly denied in this run; do not claim module access is restricted."); + } const opsContext = isPlainObject(context?.contexts?.ops) ? context.contexts.ops : {}; if (opsContext.opsWorkspaceSlug || opsContext.opsProjectId) { lines.push(