fix: restrict AI Workspace denial text to explicit denies

This commit is contained in:
Codex 2026-06-25 10:13:39 +03:00
parent 5953b1bd0e
commit 1aceabee44
6 changed files with 40 additions and 18 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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, "<redacted>");
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,
};
}

View File

@ -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(