Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ product overview.

## Unreleased

- Supply each attempt's resolved admin permission bypass decision to the assistant, so it can
distinguish its actual launch permissions from the channel's Admin label and writable tools.
Permission policies and container boundaries are unchanged.

- Preserve existing unavailable Cloud MCP selections when saving unrelated admin settings.
Removing a selection refreshes the channel picker and prevents later saves from restoring it.

Expand Down
4 changes: 4 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,10 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks.
configured model/effort and fresh/resumed session state, including clean mode, model retries,
session recovery and cross-engine fallback. The assistant is told to distinguish configured
aliases from provider-reported identity and leave unexposed harness defaults unknown.
The access preamble also states the launch's resolved admin permission bypass decision,
including resumed, background and fallback attempts. It distinguishes this decision from
channel Admin mode, writable tools and Codex's internal sandbox/approval settings; callers
without a resolved decision report unknown. The restricted memory reviewer states disabled.
→ TEST-PLAN: Per-attempt runtime identity.

- **Disposable Linux lifecycle evidence:** the manually dispatchable Linux lifecycle workflow
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ catalog, including edge cases and links to regression coverage.
removes the container boundary for that thread, so use it only for trusted host operations.
- **Read-only, Worker and Admin modes:** choose the base permission level. Auto review and Lean
context are separate options; permission bypass requires both an admin author and Admin mode.
Each attempt tells the assistant whether its actual launch has admin permission bypass;
the channel's Admin label and writable container tools do not establish that permission.
- **Approval controls:** interactive approvals and automatic review apply according to mode;
tool permissions remain separate from the container's filesystem boundary.
- **User and guest access:** select approved-member access, admins only or locked access,
Expand Down
17 changes: 17 additions & 0 deletions TEST-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4065,6 +4065,23 @@ pass. Many checks are manual (require a real Slack workspace + an authenticated

## Per-attempt runtime identity

- [x] Resolved permission facts: `test/runtime-identity-preamble.test.js` compares delivered
preambles with actual fixture CLI bypass flags for Claude and Codex members/admins in Admin
mode, resumed turns, an admin background agent, a Clean member, guarded home mounts and
cross-engine fallback. Existing model retry/healing cases check the same agreement.
`test/runtime-access-facts.test.js` keeps an absent decision unknown regardless of channel
flags or mounted home; `test/memory-review.test.js` requires the reviewer's explicit false
decision in both its launch options and prompt. These tests prove delivery, not live answers.
- [ ] Live permission-report retest, Claude and Codex separately (existing Slack Access runtime
acceptance): reserve an owned Admin-mode fixture with Auto/Lean/network off, preserve the
operator's whole-home setting, and use an approved member plus an organization admin. Ask
each to create a distinct proof file containing `access proof` and report whether permission
bypass is active in this attempt. Require member **disabled** and admin **enabled** in the
answers, matching exact-root `run_config.dangerouslySkip`, delivered per-attempt facts and
actual launch flags. A writable container or Codex `danger-full-access`/`approval_policy=never`
does not establish admin bypass. Repeat on a resumed member turn; retain the original failed
answer and append retest evidence. Keep all existing Access native-save, admission, Lean,
network, selector and restoration gates; remove proof files and restore owned settings.
- [x] `test/runtime-identity-preamble.test.js` inspects actual stub process argv through the
container-shaped fake runtime: fresh/resumed Claude and Codex turns with a changed thread
model, clean runs with unexposed defaults, rejected-model replacement, cross-engine fallback
Expand Down
6 changes: 5 additions & 1 deletion src/gateway/memory-review.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { recordUsage } from "./usage.js";
import { logEvent } from "../util/logger.js";
import { postNotice } from "../platforms/notify.js";
import { allowedFsRoot } from "../web/security.js";
import { permissionBypassNote } from "./runtime-access.js";

export const REVIEW_ORIGIN = "memory_review";
export const REVIEW_TOOLSET = "memory-review";
Expand Down Expand Up @@ -148,7 +149,9 @@ export async function runMemoryReview({
const snapshot = await readMemorySnapshot(cwd, meta);
const transcript = String((await fetchTranscript?.()) || "").trim();
if (!transcript) return { skipped: "no-transcript", saved: 0 };
const prompt = buildMemoryReviewPrompt({ snapshot, transcript, channelName: meta.name || slug });
const dangerouslySkip = false; // The reduced-tool reviewer never receives the admin bypass.
const prompt = permissionBypassNote(dangerouslySkip)
+ buildMemoryReviewPrompt({ snapshot, transcript, channelName: meta.name || slug });
const model = getMemoryReviewModel();

const target = resolveTarget(slug, meta);
Expand Down Expand Up @@ -205,6 +208,7 @@ export async function runMemoryReview({
const result = await run({
cwd,
prompt,
dangerouslySkip,
sessionId: randomUUID(),
isNewSession: true,
mcpConfig: mcpConfigFile,
Expand Down
4 changes: 2 additions & 2 deletions src/gateway/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -1445,7 +1445,7 @@ export async function runMessage({ channelId, authorId, workspaceId = "", text,
// them per-prompt even in clean mode: they expose no memory, optional skills or connectors.
const prompt = (fresh && !forkSourceSessionId ? memoryPrefix : "") + composioIdentityPrefix + channelCredentialsPrefix
+ runtimeIdentityPreamble({ engine, model: modelOverride, effort, fresh })
+ runtimeAccessPreamble(target, { clean, allowNetwork: Boolean(meta.allowNetwork) })
+ runtimeAccessPreamble(target, { clean, allowNetwork: Boolean(meta.allowNetwork), dangerouslySkip })
+ perplexityResearchPreamble(target, { clean })
+ (promptOverride ?? turnText);
return adapter.run(validateRunContext({
Expand Down Expand Up @@ -1686,7 +1686,7 @@ export async function runMessage({ channelId, authorId, workspaceId = "", text,
principal: { kind: untrustedPrincipal ? "daemon" : PRINCIPAL_KIND_BY_ORIGIN[origin] || "daemon", id: authorId }, origin, cwd,
prompt: (fbFresh ? memoryPrefix : "") + composioIdentityPrefix + channelCredentialsPrefix
+ runtimeIdentityPreamble({ engine: fallbackEngine, model: modelOverride, effort: "", fresh: fbFresh })
+ runtimeAccessPreamble(target, { clean, allowNetwork: Boolean(meta.allowNetwork) }) + perplexityResearchPreamble(target, { clean }) + fbPrompt,
+ runtimeAccessPreamble(target, { clean, allowNetwork: Boolean(meta.allowNetwork), dangerouslySkip }) + perplexityResearchPreamble(target, { clean }) + fbPrompt,
session: { id: fbSid, fresh: fbFresh }, policy: fallbackConfinement,
// The SAME runtime target: failing over to the other harness changes which CLI runs, not
// which machine boundary the channel runs behind.
Expand Down
15 changes: 14 additions & 1 deletion src/gateway/runtime-access.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,23 @@ export function containerAccessNote(target) {
return `**Container access for this run:** gateway setting \`containerFullAccessHome\` is **${setting}**. ${access} The operator-home mount is available ONLY to a trusted admin author's run in an Admin/Full-access channel while that gateway switch is on; with the switch off, Admin adds no home mount. The container remains the filesystem/process boundary. \`$HOME\` and \`~\` still refer to the channel's own home volume, not the operator's home. See the \`gateway-usage\` skill's \`references/administration.md\` for the boundary and the optional grant.`;
}

export function runtimeAccessPreamble(target, { clean, allowNetwork } = {}) {
// The launcher supplies its resolved decision. Channel mode, writable tools and mounted home
// cannot establish the current author's bypass, and a caller that omits it must stay unknown.
export function permissionBypassNote(dangerouslySkip) {
const state = typeof dangerouslySkip === "boolean" ? (dangerouslySkip ? "enabled" : "disabled") : "unknown";
return `Admin permission bypass for THIS attempt: **${state}**. `
+ (state === "unknown"
? "The resolved launch decision was not supplied; do not infer it from the channel's Admin mode. "
: "This is the gateway's resolved admin-only bypass decision for this launch; use it when asked whether permission bypass is active. ")
+ "Worker file writes, a writable container, Codex danger-full-access and approval_policy=never do not establish gateway admin bypass. "
+ "Permission bypass and filesystem mount reach are separate facts.\n";
}

export function runtimeAccessPreamble(target, { clean, allowNetwork, dangerouslySkip } = {}) {
return "[Gateway container access for THIS attempt]\n"
+ "These current access facts supersede earlier turns and generic claims about host isolation. "
+ "Answer access questions from this resolved runtime, even when the conversation previously said otherwise.\n"
+ permissionBypassNote(dangerouslySkip)
+ containerAccessNote(target) + "\n"
+ gatewayStoreAccessNote(target) + "\n"
+ "Respect the user's requested scope when checking access. For requests limited to existence, metadata or permission checks, use resolved mount facts and non-mutating metadata checks only. Do not read file contents or create, modify, or delete probe files, even temporarily. If metadata cannot establish write access, report it as unverified; do not upgrade an access-check request into a write test.\n"
Expand Down
4 changes: 3 additions & 1 deletion test/memory-review.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ test("a review marks the channel live WITHOUT an owner, and releases it after",
await review.runMemoryReview({
channelId: "C_MEM_REVIEW_LIVE", slug: entry.slug, threadKey: "7000.009", authorId: "U_MEM_REVIEW", meta, resolveTarget,
fetchTranscript: async () => "Alex: hi\nRobin: hello",
run: async () => {
run: async (options) => {
assert.equal(options.dangerouslySkip, false);
assert.match(options.prompt, /Admin permission bypass for THIS attempt: \*\*disabled\*\*/);
during = liveSnapshot().filter((e) => e.channelId === "C_MEM_REVIEW_LIVE");
return { content: "NOTHING_TO_SAVE", durationMs: 1, costUSD: 0, usage: {} };
},
Expand Down
12 changes: 12 additions & 0 deletions test/runtime-access-facts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,21 @@ test("missing resolved facts stay unknown instead of inferring a mode or host is
assert.match(note, /database visibility are unknown/);
assert.match(note, /Clean mode for this attempt is unknown/);
assert.match(note, /Network policy for this attempt is unknown/);
assert.match(note, /Admin permission bypass for THIS attempt: \*\*unknown\*\*/);
assert.doesNotMatch(note, /runtime directory: \*\*(not )?mounted\*\*/);
});

test("permission facts use the supplied launch decision, never channel mode or mounted home", () => {
const home = target([bind(path.dirname(gatewayRoot()), undefined, "operator-home")]);
home.meta = { adminMode: true, autoMode: true };
for (const dangerouslySkip of [false, true]) {
const note = runtimeAccessPreamble(home, { dangerouslySkip });
assert.match(note, new RegExp(`Admin permission bypass for THIS attempt: \\*\\*${dangerouslySkip ? "enabled" : "disabled"}\\*\\*`));
assert.match(note, /danger-full-access.*approval_policy.*never.*do not establish.*admin bypass/);
}
assert.match(runtimeAccessPreamble(home), /Admin permission bypass for THIS attempt: \*\*unknown\*\*/);
});

test("a sudo-host target states that the container boundary is absent", () => {
const note = runtimeAccessPreamble({ backend: "host", container: null }, { clean: false, allowNetwork: true });
assert.match(note, /Direct host access.*enabled by `\/sudo`/s);
Expand Down
62 changes: 62 additions & 0 deletions test/runtime-identity-preamble.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,71 @@ function metadata(call) {
assert.match(prompt, /For requests limited to existence, metadata or permission checks, use resolved mount facts and non-mutating metadata checks only/);
assert.match(prompt, /Do not read file contents or create, modify, or delete probe files, even temporarily/);
assert.match(prompt, /If metadata cannot establish write access, report it as unverified/);
assertBypassMatchesLaunch(call);
return JSON.parse(match[1]);
}

function assertBypassMatchesLaunch(call) {
const prompt = call.args.find((arg) => String(arg).includes("[Gateway runtime for THIS attempt:"));
const bypass = call.args.some((arg) => ["--dangerously-skip-permissions", "--dangerously-bypass-approvals-and-sandbox"].includes(arg));
assert.match(prompt, new RegExp(`Admin permission bypass for THIS attempt: \\*\\*${bypass ? "enabled" : "disabled"}\\*\\*`));
return bypass;
}

for (const engine of ["claude", "codex"]) {
test(`${engine}: a guarded home mount does not make a member's bypass fact true`, async (t) => {
const context = await fixture(`PERMISSION_GUARD_${engine}`, engine, engine === "claude" ? "sonnet" : "gpt-5.6-sol", { adminMode: true });
const previousPrepare = backend.prepareTarget;
backend.prepareTarget = (base) => {
const target = previousPrepare(base);
// Exercise the retained home guard for a widened target; normal member targets have no
// operator-home mount. This fake backend executes only fixture CLIs, never a container.
target.container.mounts = [...target.container.mounts, { type: "bind", kind: "operator-home", source: os.homedir(), target: os.homedir() }];
return target;
};
t.after(() => { backend.prepareTarget = previousPrepare; });
const start = backend.calls.spawn.length;
await runMessage({ ...context, text: "Does the guarded mount grant admin bypass?" });
const [call] = attempts(start);
assert.equal(assertBypassMatchesLaunch(call), false);
if (engine === "codex") assert.equal(call.args[call.args.indexOf("--sandbox") + 1], "read-only");
});

test(`${engine}: permission facts follow actual author and origin across fresh, resumed and Clean attempts`, async () => {
const context = await fixture(`PERMISSION_FACTS_${engine}`, engine, engine === "claude" ? "sonnet" : "gpt-5.6-sol", { adminMode: true, autoMode: false });
saveSettings({ containerFullAccessHome: true });
const start = backend.calls.spawn.length;
await runMessage({ ...context, text: "Does Admin mode give this member permission bypass?" });
await runMessage({ ...context, text: "Does a successful write change the bypass fact?" });
await setUser(context.authorId, { name: "Permission admin", approved: true, isAdmin: true });
await runMessage({ ...context, text: "Report the actual admin bypass for this attempt." });
await runMessage({ ...context, origin: "background_agent", text: "Report the actual bypass for this unattended attempt." });
await setUser(context.authorId, { name: "Permission member", approved: true, isAdmin: false });
await patchChannelMeta(context.entry.slug, { cleanMode: true });
await runMessage({ ...context, text: "Report bypass in this member's Clean attempt." });
const calls = attempts(start);
assert.equal(calls.length, 5);
assert.deepEqual(calls.map(assertBypassMatchesLaunch), [false, false, true, false, false]);
assert.equal(metadata(calls[1]).session, "resumed");
const memberPrompt = calls[0].args.find((arg) => String(arg).includes("[Gateway runtime for THIS attempt:"));
assert.match(memberPrompt, /resolved runtime has no operator-home mount/);
const adminPrompt = calls[2].args.find((arg) => String(arg).includes("[Gateway runtime for THIS attempt:"));
assert.match(adminPrompt, /resolved runtime includes the operator-home mount/);
});
}

test("admin bypass facts reach the actual fallback engine as well as the primary", async () => {
const context = await fixture("PERMISSION_FALLBACK", "claude", "sonnet", { adminMode: true });
await setUser(context.authorId, { name: "Fallback admin", approved: true, isAdmin: true });
const start = backend.calls.spawn.length;
await runMessage({ ...context, text: "CLAUDE_STUB_LIMIT_FAIL_SAFE" });
const calls = attempts(start);
assert.equal(calls.length, 2);
assert.deepEqual(calls.map(assertBypassMatchesLaunch), [true, true]);
assert.equal(metadata(calls[0]).engine, "claude");
assert.equal(metadata(calls[1]).engine, "codex");
});

for (const engine of ["claude", "codex"]) {
test(`${engine}: network policy changes reach fresh and resumed prompts without exposing Clean credentials`, async () => {
const context = await fixture(`NETWORK_FACTS_${engine}`, engine, engine === "claude" ? "sonnet" : "gpt-5.6-sol", { allowNetwork: true });
Expand Down
Loading