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
11 changes: 7 additions & 4 deletions docs/codex-cloud.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,10 +382,13 @@ copying credentials into the checkout.
but it cannot inject host MCP tools or retroactively grant OAuth. Restart the MCP client or open
a new task after consent. When the host exposes its app identifiers, pass that non-secret
inventory to the environment check with
`--hosted-app-inventory=github,railway,supabase`. The checker rejects stale `railway_cloud` and
unrecognized names (so tokens/secrets are never accepted), reports only allowlisted presence
flags in the sanitized capability lines, and leaves the inventory explicitly unverified when the
host does not provide it; repository config is never substituted for this evidence.
`--hosted-app-inventory=github,railway,supabase`. The exact `=` form is required and the option
may appear only once; every other unsupported argument fails closed. The checker rejects stale
`railway_cloud`, shared sensitive-token patterns, and identifiers outside its reviewed connector
allowlist. It normalizes connector-name case and never echoes supplied values. Invalid input
suppresses capability output rather than printing a misleading unverified state. Inventory
remains explicitly unverified when the host does not provide it; repository config is never
substituted for this evidence.
4. **Prove the shell boundary before providers.** First run the direct raw-shell command above
before profiles or command shims. Then run `npm run check:codex-cloud`,
`npm run check:codex-cloud -- --runtime`, `npm run check:runtime`, and
Expand Down
55 changes: 46 additions & 9 deletions scripts/check-codex-cloud-setup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
hasSafeGitHubCredentialHelper,
inspectOriginRemote,
} from "./ensure-codex-cloud-git-remote.mjs";
import { redactSensitiveText } from "./sensitive-text.mjs";
import { providerEnvironmentKeys } from "./test-environment.mjs";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
Expand Down Expand Up @@ -230,6 +231,8 @@ export const knownHostedAppInventoryNames = Object.freeze([
"supabase",
"figma",
"sentry",
"slack",
"linear",
"figma_cloud",
"sentry_cloud",
"supabase_cloud",
Expand All @@ -255,6 +258,28 @@ export function parseHostedAppInventoryArgument(args) {
.filter(Boolean);
}

/**
* Reject malformed or ambiguous attempts to supply hosted inventory evidence.
* @param {string[]} args
* @returns {string[]}
*/
export function validateHostedAppInventoryArguments(args) {
const exactPrefix = "--hosted-app-inventory=";
const exactArguments = args.filter((value) => value.startsWith(exactPrefix));
const supportedArguments = new Set(["--environment", "--runtime"]);
const unsupportedArguments = args.filter((value) => !supportedArguments.has(value) && !value.startsWith(exactPrefix));
const errors = [];
if (unsupportedArguments.length > 0) {
errors.push(
"Unsupported Cloud-check argument; hosted app inventory must use exactly --hosted-app-inventory=<comma-separated-apps>.",
);
}
if (exactArguments.length > 1) {
errors.push("Hosted app inventory may be supplied only once.");
}
return errors;
}

/**
* Format hosted-app inventory for sanitized capability output.
* Never echo arbitrary operator-supplied strings — only allowlisted presence flags.
Expand All @@ -263,8 +288,9 @@ export function parseHostedAppInventoryArgument(args) {
*/
export function hostedAppInventoryCapabilityLine(appNames) {
if (appNames === null) return "hosted_app.inventory=external-unverified-until-fresh-task";
const set = new Set(appNames);
const unknownCount = appNames.filter(
const normalizedNames = appNames.map((name) => name.toLowerCase());
const set = new Set(normalizedNames);
const unknownCount = normalizedNames.filter(
(name) => !knownHostedAppInventoryNames.includes(name) && !staleHostedAppInventoryNames.includes(name),
).length;
const parts = [
Expand All @@ -289,22 +315,29 @@ export function validateHostedAppInventory(appNames) {
errors.push("Hosted app inventory was supplied but contained no app names.");
return errors;
}
const invalidNames = appNames.filter((name) => !/^[A-Za-z0-9_.-]+$/.test(name));
const normalizedNames = appNames.map((name) => name.toLowerCase());
const invalidNames = normalizedNames.filter((name) => !/^[A-Za-z0-9_.-]+$/.test(name));
if (invalidNames.length > 0) {
errors.push("Hosted app inventory names may contain only letters, numbers, dot, underscore, and hyphen.");
}
const unrecognizedNames = appNames.filter(
const credentialShapedNames = appNames.filter((name) => redactSensitiveText(name) !== name || name.length > 128);
if (credentialShapedNames.length > 0) {
errors.push(
"Hosted app inventory appears to contain a credential; supply connector names only, never tokens or secrets.",
);
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
const unrecognizedNames = normalizedNames.filter(
(name) =>
/^[A-Za-z0-9_.-]+$/.test(name) &&
!knownHostedAppInventoryNames.includes(name) &&
!staleHostedAppInventoryNames.includes(name),
);
if (unrecognizedNames.length > 0) {
errors.push(
`Hosted app inventory contains unrecognized app names; supply only connector names (${knownHostedAppInventoryNames.join(", ")}), never tokens or secrets.`,
"Hosted app inventory contains unrecognized connector identifiers; update the checker allowlist before accepting them as evidence.",
);
}
if (appNames.some((name) => staleHostedAppInventoryNames.includes(name))) {
if (normalizedNames.some((name) => staleHostedAppInventoryNames.includes(name))) {
errors.push(
"Hosted app inventory contains stale railway_cloud; remove or reconnect that host-local app, then start a fresh task and supply the new inventory.",
);
Expand Down Expand Up @@ -1025,8 +1058,12 @@ export async function validateCodexCloudRuntime(env = process.env) {

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const errors = validateCodexCloudSetup();
const hostedAppInventory = parseHostedAppInventoryArgument(process.argv.slice(2));
errors.push(...validateHostedAppInventory(hostedAppInventory));
const commandArguments = process.argv.slice(2);
const hostedAppArgumentErrors = validateHostedAppInventoryArguments(commandArguments);
const hostedAppInventory = parseHostedAppInventoryArgument(commandArguments);
const hostedAppInventoryErrors = validateHostedAppInventory(hostedAppInventory);
const hostedAppInputErrors = [...hostedAppArgumentErrors, ...hostedAppInventoryErrors];
errors.push(...hostedAppInputErrors);
const runtime = process.argv.includes("--runtime");
const environment = runtime || process.env.CODEX_CLOUD === "1" || process.argv.includes("--environment");
if (runtime) errors.push(...(await validateCodexCloudRuntime()));
Expand All @@ -1040,7 +1077,7 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
"[Codex Cloud Check] WARN: checkout freshness is unverified during provisioning; run explicit acceptance with CODEX_CLOUD_EXPECTED_BASE_SHA.",
);
}
if (environment) {
if (environment && hostedAppInputErrors.length === 0) {
console.log("[Codex Cloud Environment] sanitized effective modes and capabilities:");
for (const line of sanitizedCloudCapabilityLines(process.env, { hostedAppInventory })) console.log(` ${line}`);
}
Expand Down
42 changes: 38 additions & 4 deletions tests/codex-cloud-setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
validateCodexCloudEnvironment,
validateCodexProjectMcpConfiguration,
validateHostedAppInventory,
validateHostedAppInventoryArguments,
validateMcpConfiguration,
} from "../scripts/check-codex-cloud-setup.mjs";
import { providerEnvironmentKeys } from "../scripts/test-environment.mjs";
Expand Down Expand Up @@ -446,15 +447,25 @@ describe("Codex Cloud environment contract", () => {
"Hosted app inventory contains stale railway_cloud; remove or reconnect that host-local app, then start a fresh task and supply the new inventory.",
);
expect(validateHostedAppInventory(["github", "railway", "supabase"])).toEqual([]);
const secretShaped = "sk-abcdef0123456789tokenvalue";
expect(validateHostedAppInventory(["GitHub", "Slack", "Linear"])).toEqual([]);
expect(validateHostedAppInventory(["github", "custom-app"])).toContain(
"Hosted app inventory contains unrecognized connector identifiers; update the checker allowlist before accepting them as evidence.",
);
const secretShaped = ["xoxb", "12345678"].join("-");
expect(validateHostedAppInventory(["github", secretShaped])).toContain(
"Hosted app inventory contains unrecognized app names; supply only connector names (github, railway, supabase, figma, sentry, figma_cloud, sentry_cloud, supabase_cloud), never tokens or secrets.",
"Hosted app inventory appears to contain a credential; supply connector names only, never tokens or secrets.",
);
const jwtShaped = [["eyJ", "abcdefgh"].join(""), "ijklmnop", "qrstuvwx"].join(".");
expect(validateHostedAppInventory(["github", jwtShaped])).toContain(
"Hosted app inventory appears to contain a credential; supply connector names only, never tokens or secrets.",
);
const capabilityLine = hostedAppInventoryCapabilityLine(["github", "railway", "supabase"]);
expect(capabilityLine).toContain("hosted_app.inventory=provided count=3");
const capabilityLine = hostedAppInventoryCapabilityLine(["GitHub", "railway", "supabase", "Slack", "Linear"]);
expect(capabilityLine).toContain("hosted_app.inventory=provided count=5");
expect(capabilityLine).toContain("github=true");
expect(capabilityLine).toContain("railway=true");
expect(capabilityLine).toContain("supabase=true");
expect(capabilityLine).toContain("slack=true");
expect(capabilityLine).toContain("linear=true");
expect(capabilityLine).toContain("stale_railway_cloud=false");
expect(capabilityLine).toContain("unknown=0");
const leaked = hostedAppInventoryCapabilityLine(["github", secretShaped]);
Expand All @@ -463,6 +474,29 @@ describe("Codex Cloud environment contract", () => {
expect(
sanitizedCloudCapabilityLines({}, { hostedAppInventory: ["github", secretShaped] }).join("\n"),
).not.toContain(secretShaped);
expect(validateHostedAppInventoryArguments(["--hosted-app-inventory", "github,railway"])).toContain(
"Unsupported Cloud-check argument; hosted app inventory must use exactly --hosted-app-inventory=<comma-separated-apps>.",
);
expect(validateHostedAppInventoryArguments(["--hosted-app-inventroy=github,railway"])).toContain(
"Unsupported Cloud-check argument; hosted app inventory must use exactly --hosted-app-inventory=<comma-separated-apps>.",
);
expect(
validateHostedAppInventoryArguments(["--hosted-app-inventory=github", "--hosted-app-inventory=railway"]),
).toContain("Hosted app inventory may be supplied only once.");
expect(validateHostedAppInventoryArguments(["--environment", "--runtime"])).toEqual([]);

const malformedCli = spawnSync(
process.execPath,
[
fileURLToPath(new URL("../scripts/check-codex-cloud-setup.mjs", import.meta.url)),
"--environment",
"--hosted-app-inventroy=github",
],
{ cwd: repoRoot, encoding: "utf8", env: { PATH: process.env.PATH, NODE_ENV: "test" } },
);
expect(malformedCli.status).toBe(1);
expect(malformedCli.stderr).toContain("Unsupported Cloud-check argument");
expect(malformedCli.stdout).not.toContain("hosted_app.inventory=");
});

it("probes the raw task environment without printing credential values", () => {
Expand Down
Loading