mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-21 01:51:39 -06:00
fix(qa): isolate packaged mock auth config
Punchcard-Session: frost-orchard-lantern-ze
This commit is contained in:
@@ -1505,9 +1505,19 @@ jobs:
|
||||
"$RUNTIME_ROOT"/tmp/openclaw-"$RUNNER_UID"/openclaw-qa-suite-*) ;;
|
||||
*) echo "SUT temp root escaped the workflow runtime root." >&2; exit 1 ;;
|
||||
esac
|
||||
requested_config_path="${OPENCLAW_CONFIG_PATH:?}"
|
||||
config_path="${temp_root}/openclaw.json"
|
||||
[[ -f "$config_path" && ! -L "$config_path" ]]
|
||||
[[ "$(realpath -e "$config_path")" == "$config_path" ]]
|
||||
case "$requested_config_path" in
|
||||
"$config_path") ;;
|
||||
"${temp_root}/state/qa-auth-bootstrap/openclaw.json")
|
||||
[[ "${1:-}" == "models" && "${2:-}" == "auth" ]]
|
||||
[[ -f "$requested_config_path" && ! -L "$requested_config_path" ]]
|
||||
[[ "$(realpath -e "$requested_config_path")" == "$requested_config_path" ]]
|
||||
;;
|
||||
*) echo "SUT config path escaped the canonical or auth-bootstrap roots." >&2; exit 1 ;;
|
||||
esac
|
||||
capture_live_model_config "$config_path"
|
||||
|
||||
export OPENCLAW_QA_TEMP_ROOT="$temp_root"
|
||||
@@ -1515,7 +1525,7 @@ jobs:
|
||||
export OPENCLAW_HOME="$HOME"
|
||||
export OPENCLAW_STATE_DIR="${temp_root}/state"
|
||||
export OPENCLAW_OAUTH_DIR="${OPENCLAW_STATE_DIR}/credentials"
|
||||
export OPENCLAW_CONFIG_PATH="$config_path"
|
||||
export OPENCLAW_CONFIG_PATH="$requested_config_path"
|
||||
export XDG_CACHE_HOME="${temp_root}/xdg-cache"
|
||||
export XDG_CONFIG_HOME="${temp_root}/xdg-config"
|
||||
export XDG_DATA_HOME="${temp_root}/xdg-data"
|
||||
@@ -1550,6 +1560,9 @@ jobs:
|
||||
chown -R "$SUT_UID:$SUT_GID" "$path"
|
||||
chmod -R u=rwX,go= "$path"
|
||||
done
|
||||
if [[ "$requested_config_path" != "$config_path" ]]; then
|
||||
[[ "$(stat -c '%F:%a:%u:%g' "$requested_config_path")" == "regular file:600:${SUT_UID}:${SUT_GID}" ]]
|
||||
fi
|
||||
if [[ -n "${OPENCLAW_BUNDLED_PLUGINS_DIR:-}" ]]; then
|
||||
chown -R root:root "$OPENCLAW_BUNDLED_PLUGINS_DIR"
|
||||
chmod -R a+rX,go-w "$OPENCLAW_BUNDLED_PLUGINS_DIR"
|
||||
@@ -1615,6 +1628,7 @@ jobs:
|
||||
export SUT_UID SUT_GID RUNNER_UID RUNNER_GID RUNNER_HOME RUNNER_TEMP_DIR
|
||||
export CANDIDATE_ROOT CANDIDATE_ARTIFACTS_DIR RUNTIME_ROOT NODE_BIN
|
||||
export PRELOAD_PATH RUNNER_SENTINEL TRUSTED_WORKSPACE EVIDENCE_ROOT
|
||||
export CANONICAL_CONFIG_PATH="$config_path"
|
||||
export boundary_mode generation command_file identity_file sandbox_file
|
||||
export command_sha256 expected_env_keys_b64 sandbox_payload_b64
|
||||
|
||||
@@ -1624,6 +1638,8 @@ jobs:
|
||||
chmod 0600 "$launcher_stage_file"
|
||||
trap 'rm -f "$launcher_stage_file"' EXIT
|
||||
export launcher_stage_file
|
||||
export -n BASHOPTS SHELLOPTS
|
||||
unset BASH_ENV ENV
|
||||
/usr/bin/unshare \
|
||||
--mount \
|
||||
--fork \
|
||||
@@ -1776,8 +1792,12 @@ jobs:
|
||||
runtime_stage=verify-runtime-files
|
||||
[[ -r "$CANDIDATE_ROOT/dist/index.js" &&
|
||||
! -w "$CANDIDATE_ROOT/dist/index.js" &&
|
||||
-r "${OPENCLAW_CONFIG_PATH:?}" &&
|
||||
! -w "$OPENCLAW_CONFIG_PATH" ]]
|
||||
-r "${CANONICAL_CONFIG_PATH:?}" &&
|
||||
! -w "$CANONICAL_CONFIG_PATH" ]]
|
||||
if [[ "$OPENCLAW_CONFIG_PATH" != "$CANONICAL_CONFIG_PATH" ]]; then
|
||||
[[ "${1:-}" == "models" && "${2:-}" == "auth" &&
|
||||
-r "$OPENCLAW_CONFIG_PATH" && -w "$OPENCLAW_CONFIG_PATH" ]]
|
||||
fi
|
||||
[[ -d "${CANDIDATE_ARTIFACTS_DIR:?}" && -r "$CANDIDATE_ARTIFACTS_DIR" && -x "$CANDIDATE_ARTIFACTS_DIR" && ! -w "$CANDIDATE_ARTIFACTS_DIR" ]]
|
||||
for writable_path in \
|
||||
"${OPENCLAW_QA_TEMP_ROOT:?}/workspace" \
|
||||
@@ -1814,6 +1834,7 @@ jobs:
|
||||
unset \
|
||||
CANDIDATE_ROOT \
|
||||
CANDIDATE_ARTIFACTS_DIR \
|
||||
CANONICAL_CONFIG_PATH \
|
||||
EVIDENCE_ROOT \
|
||||
NODE_BIN \
|
||||
PRELOAD_PATH \
|
||||
@@ -1855,9 +1876,6 @@ jobs:
|
||||
else
|
||||
runtime_node_args=("$runtime_candidate_root/dist/index.js" "$@")
|
||||
fi
|
||||
# Login Bash reads /etc/bash.bashrc with inherited nounset.
|
||||
# Add PS1 only after the attested inbound env-key comparison.
|
||||
export PS1=
|
||||
runtime_stage=exec-runtime
|
||||
exec "$runtime_node_bin" "${runtime_node_args[@]}"
|
||||
'\'' openclaw-sut "$@"
|
||||
|
||||
@@ -160,22 +160,32 @@ if (!recordPath || !configPath || !stateDir) {
|
||||
throw new Error("missing fixture environment");
|
||||
}
|
||||
const record = (value) => fs.appendFileSync(recordPath, JSON.stringify(value) + "\\n");
|
||||
const authDbPath = path.join(stateDir, "agents", "qa", "agent", "openclaw-agent.sqlite");
|
||||
if (args[0] === "models") {
|
||||
let stdin = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
for await (const chunk of process.stdin) stdin += chunk;
|
||||
const provider = args[args.indexOf("--provider") + 1];
|
||||
const configStat = fs.lstatSync(configPath);
|
||||
record({
|
||||
kind: "auth",
|
||||
args,
|
||||
stdin,
|
||||
dbExists: fs.existsSync(path.join(stateDir, "agents", "qa", "agent", "openclaw-agent.sqlite")),
|
||||
authDbPath,
|
||||
dbExists: fs.existsSync(authDbPath),
|
||||
configPath,
|
||||
configMode: configStat.mode & 0o777,
|
||||
configRegular: configStat.isFile(),
|
||||
configSymlink: configStat.isSymbolicLink(),
|
||||
stateDir,
|
||||
env: {
|
||||
OPENCLAW_CLI: process.env.OPENCLAW_CLI,
|
||||
OPENCLAW_CONFIG_PATH: configPath,
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
},
|
||||
});
|
||||
fs.mkdirSync(path.dirname(authDbPath), { recursive: true });
|
||||
fs.writeFileSync(authDbPath, "fixture auth");
|
||||
if (process.env.QA_FAIL_PROVIDER === provider) {
|
||||
process.stderr.write("Authorization: Bearer " + stdin.trim());
|
||||
process.exit(9);
|
||||
@@ -186,7 +196,16 @@ if (args[0] === "models") {
|
||||
process.exit(0);
|
||||
}
|
||||
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||
record({ kind: "gateway", args, fixtureProfiles: config.fixtureProfiles });
|
||||
record({
|
||||
kind: "gateway",
|
||||
args,
|
||||
authDbPath,
|
||||
dbExists: fs.existsSync(authDbPath),
|
||||
configPath,
|
||||
authProfileIds: Object.keys(config.auth?.profiles ?? {}),
|
||||
fixtureProfiles: config.fixtureProfiles,
|
||||
stateDir,
|
||||
});
|
||||
process.stderr.write("fixture gateway exit");
|
||||
process.exit(17);
|
||||
`,
|
||||
@@ -1441,16 +1460,28 @@ describe("buildQaRuntimeEnv", () => {
|
||||
],
|
||||
]);
|
||||
for (const record of authRecords) {
|
||||
expect(record.dbExists).toBe(false);
|
||||
expect(record.stdin).toMatch(/^sk-qa-mock-[a-f0-9]{32}\n$/u);
|
||||
expect(record.env).toMatchObject({
|
||||
OPENCLAW_CLI: "1",
|
||||
});
|
||||
expect(record.configMode).toBe(0o600);
|
||||
expect(record.configRegular).toBe(true);
|
||||
expect(record.configSymlink).toBe(false);
|
||||
}
|
||||
expect(authRecords.map((record) => record.dbExists)).toEqual([false, true]);
|
||||
const authConfigPaths = authRecords.map((record) => String(record.configPath));
|
||||
expect(new Set(authConfigPaths).size).toBe(1);
|
||||
expect(authConfigPaths[0]).toBe(
|
||||
path.join(String(authRecords[0]?.stateDir), "qa-auth-bootstrap", "openclaw.json"),
|
||||
);
|
||||
expect(records.at(-1)).toMatchObject({
|
||||
kind: "gateway",
|
||||
fixtureProfiles: ["openai", "anthropic"],
|
||||
authProfileIds: ["qa-mock-openai", "qa-mock-anthropic"],
|
||||
dbExists: true,
|
||||
});
|
||||
expect(records.at(-1)?.configPath).not.toBe(authConfigPaths[0]);
|
||||
expect(records.at(-1)?.fixtureProfiles).toBeUndefined();
|
||||
expect(new Set(records.map((record) => record.authDbPath)).size).toBe(1);
|
||||
});
|
||||
|
||||
it("blocks packaged gateway spawn when candidate auth bootstrap fails", async () => {
|
||||
@@ -1486,7 +1517,13 @@ describe("buildQaRuntimeEnv", () => {
|
||||
);
|
||||
const records = await readJsonLines(recordPath);
|
||||
expect(records).toHaveLength(1);
|
||||
expect(records[0]).toMatchObject({ kind: "auth", dbExists: false });
|
||||
expect(records[0]).toMatchObject({
|
||||
kind: "auth",
|
||||
dbExists: false,
|
||||
configMode: 0o600,
|
||||
configRegular: true,
|
||||
configSymlink: false,
|
||||
});
|
||||
const submittedKey = String(records[0]?.stdin).trim();
|
||||
expect(submittedKey).toMatch(/^sk-qa-mock-[a-f0-9]{32}$/u);
|
||||
expect(error.message).not.toContain(submittedKey);
|
||||
|
||||
@@ -67,7 +67,11 @@ import {
|
||||
stageQaLiveApiKeyProfiles,
|
||||
stageQaLiveAnthropicSetupToken,
|
||||
} from "./providers/live-frontier/auth.js";
|
||||
import { buildQaMockProfileId, stageQaMockAuthProfiles } from "./providers/shared/mock-auth.js";
|
||||
import {
|
||||
applyQaMockAuthProfileConfig,
|
||||
buildQaMockProfileId,
|
||||
stageQaMockAuthProfiles,
|
||||
} from "./providers/shared/mock-auth.js";
|
||||
import { seedQaAgentWorkspace } from "./qa-agent-workspace.js";
|
||||
import { buildQaGatewayConfig, type QaThinkingLevel } from "./qa-gateway-config.js";
|
||||
import type { QaTransportAdapter } from "./qa-transport.js";
|
||||
@@ -189,6 +193,7 @@ function createQaPackagedMockApiKey(): string {
|
||||
|
||||
async function stageQaPackagedMockAuthProfiles(params: {
|
||||
command: QaGatewayChildCommand;
|
||||
configPath: string;
|
||||
cwd: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
providers: readonly string[];
|
||||
@@ -210,7 +215,7 @@ async function stageQaPackagedMockAuthProfiles(params: {
|
||||
buildQaMockProfileId(provider),
|
||||
],
|
||||
cwd: params.command.cwd ?? params.cwd,
|
||||
env: params.env,
|
||||
env: { ...params.env, OPENCLAW_CONFIG_PATH: params.configPath },
|
||||
stdin: `${createQaPackagedMockApiKey()}\n`,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -283,6 +288,7 @@ export async function startQaGatewayChild(params: {
|
||||
const xdgDataHome = path.join(tempRoot, "xdg-data");
|
||||
const xdgCacheHome = path.join(tempRoot, "xdg-cache");
|
||||
const configPath = path.join(tempRoot, "openclaw.json");
|
||||
const packagedAuthConfigPath = path.join(stateDir, "qa-auth-bootstrap", "openclaw.json");
|
||||
const gatewayToken = `qa-suite-${randomUUID()}`;
|
||||
const transport = params.transport ?? createQaGatewayEmptyTransport();
|
||||
await seedQaAgentWorkspace({
|
||||
@@ -365,7 +371,9 @@ export async function startQaGatewayChild(params: {
|
||||
});
|
||||
const mockAuthProviders = getQaProvider(providerMode).mockAuthProviders;
|
||||
if (mockAuthProviders && mockAuthProviders.length > 0) {
|
||||
if (!usesPackagedCandidate) {
|
||||
if (usesPackagedCandidate) {
|
||||
cfg = applyQaMockAuthProfileConfig({ cfg, providers: mockAuthProviders });
|
||||
} else {
|
||||
cfg = await stageQaMockAuthProfiles({
|
||||
cfg,
|
||||
stateDir,
|
||||
@@ -391,6 +399,7 @@ export async function startQaGatewayChild(params: {
|
||||
let cfg!: OpenClawConfig;
|
||||
let getChildFailure: (() => QaChildFailure | null) | null = null;
|
||||
let env: NodeJS.ProcessEnv | null = null;
|
||||
let packagedMockAuthStaged = false;
|
||||
let migrationConvergenceRestartUsed = false;
|
||||
let reuseStartupLaunchState = false;
|
||||
|
||||
@@ -588,13 +597,29 @@ export async function startQaGatewayChild(params: {
|
||||
mode: 0o600,
|
||||
});
|
||||
const mockAuthProviders = getQaProvider(providerMode).mockAuthProviders;
|
||||
if (usesPackagedCandidate && gatewayCommand && mockAuthProviders?.length) {
|
||||
if (
|
||||
usesPackagedCandidate &&
|
||||
gatewayCommand &&
|
||||
mockAuthProviders?.length &&
|
||||
!packagedMockAuthStaged
|
||||
) {
|
||||
const canonicalConfig = await fs.readFile(configPath);
|
||||
await fs.mkdir(path.dirname(packagedAuthConfigPath), { recursive: true, mode: 0o700 });
|
||||
await fs.writeFile(packagedAuthConfigPath, canonicalConfig, {
|
||||
flag: "wx",
|
||||
mode: 0o600,
|
||||
});
|
||||
await stageQaPackagedMockAuthProfiles({
|
||||
command: gatewayCommand,
|
||||
configPath: packagedAuthConfigPath,
|
||||
cwd: gatewayCwd,
|
||||
env,
|
||||
providers: mockAuthProviders,
|
||||
});
|
||||
if (!canonicalConfig.equals(await fs.readFile(configPath))) {
|
||||
throw new Error("installed package mock auth bootstrap mutated canonical config");
|
||||
}
|
||||
packagedMockAuthStaged = true;
|
||||
}
|
||||
}
|
||||
if (!env) {
|
||||
|
||||
@@ -14,6 +14,22 @@ export function buildQaMockProfileId(provider: string): string {
|
||||
return `qa-mock-${provider}`;
|
||||
}
|
||||
|
||||
export function applyQaMockAuthProfileConfig(params: {
|
||||
cfg: OpenClawConfig;
|
||||
providers?: readonly string[];
|
||||
}): OpenClawConfig {
|
||||
let next = params.cfg;
|
||||
for (const provider of uniqueStrings(params.providers ?? QA_MOCK_AUTH_PROVIDERS)) {
|
||||
next = applyAuthProfileConfig(next, {
|
||||
profileId: buildQaMockProfileId(provider),
|
||||
provider,
|
||||
mode: "api_key",
|
||||
displayName: `QA mock ${provider} credential`,
|
||||
});
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* In mock provider modes the qa suite runs against an embedded mock server
|
||||
* instead of a real provider API. The mock does not validate credentials, but
|
||||
@@ -41,7 +57,6 @@ export async function stageQaMockAuthProfiles(params: {
|
||||
}): Promise<OpenClawConfig> {
|
||||
const agentIds = uniqueStrings(params.agentIds ?? QA_MOCK_AUTH_AGENT_IDS);
|
||||
const providers = uniqueStrings(params.providers ?? QA_MOCK_AUTH_PROVIDERS);
|
||||
let next = params.cfg;
|
||||
for (const agentId of agentIds) {
|
||||
await writeQaAuthProfiles({
|
||||
agentId,
|
||||
@@ -59,13 +74,5 @@ export async function stageQaMockAuthProfiles(params: {
|
||||
stateDir: params.stateDir,
|
||||
});
|
||||
}
|
||||
for (const provider of providers) {
|
||||
next = applyAuthProfileConfig(next, {
|
||||
profileId: buildQaMockProfileId(provider),
|
||||
provider,
|
||||
mode: "api_key",
|
||||
displayName: `QA mock ${provider} credential`,
|
||||
});
|
||||
}
|
||||
return next;
|
||||
return applyQaMockAuthProfileConfig({ cfg: params.cfg, providers });
|
||||
}
|
||||
|
||||
@@ -872,23 +872,32 @@ describe("release Telegram QA workflow", () => {
|
||||
);
|
||||
expect(createSut).not.toContain('chmod 0711 "$temp_root"');
|
||||
expect(createSut).not.toContain('chmod 1777 "$temp_root"');
|
||||
expect(createSut).toContain('"${temp_root}/state/qa-auth-bootstrap/openclaw.json")');
|
||||
expect(createSut).toContain(
|
||||
'"$(stat -c \'%F:%a:%u:%g\' "$requested_config_path")" == "regular file:600:${SUT_UID}:${SUT_GID}"',
|
||||
);
|
||||
});
|
||||
|
||||
it("adds an empty PS1 only after attested runtime environment verification", () => {
|
||||
it("strips inherited Bash startup state before the first isolated child", () => {
|
||||
const createSut = requireRun(
|
||||
"run_telegram",
|
||||
"Create isolated Telegram SUT identity and launcher",
|
||||
);
|
||||
const launcher = extractHereDocument(createSut, "LAUNCHER");
|
||||
const verification = '[[ "$actual_env_keys_b64" == "$runtime_expected_env_keys_b64" ]]';
|
||||
const ps1Export = "export PS1=";
|
||||
const stripOptions = "export -n BASHOPTS SHELLOPTS";
|
||||
const stripStartupFiles = "unset BASH_ENV ENV";
|
||||
const isolatedSpawn = "/usr/bin/unshare";
|
||||
const candidateExec = 'exec "$runtime_node_bin" "${runtime_node_args[@]}"';
|
||||
|
||||
expect(launcher.match(/export PS1=/gu)).toHaveLength(1);
|
||||
expect(launcher).not.toContain("export PS1=");
|
||||
expect(launcher.indexOf(stripOptions)).toBeGreaterThan(-1);
|
||||
expect(launcher.indexOf(stripStartupFiles)).toBeGreaterThan(launcher.indexOf(stripOptions));
|
||||
expect(launcher.indexOf(isolatedSpawn)).toBeGreaterThan(launcher.indexOf(stripStartupFiles));
|
||||
expect(launcher.indexOf(verification)).toBeGreaterThan(-1);
|
||||
expect(launcher.indexOf(ps1Export)).toBeGreaterThan(launcher.indexOf(verification));
|
||||
expect(launcher.indexOf(candidateExec)).toBeGreaterThan(launcher.indexOf(ps1Export));
|
||||
expect(launcher.indexOf(candidateExec)).toBeGreaterThan(launcher.indexOf(verification));
|
||||
expect(launcher.match(/exec "\$runtime_node_bin"/gu)).toHaveLength(1);
|
||||
expect(launcher.match(/\/bin\/bash -ceu/gu)).toHaveLength(2);
|
||||
expect(launcher).toContain('grep -Ev "^(PWD|SHLVL|_)$"');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user