Skip to content

Commit 631222d

Browse files
committed
fix(secrets): accept inactive refs from partial fallback
1 parent 6252f9b commit 631222d

2 files changed

Lines changed: 69 additions & 14 deletions

File tree

src/cli/command-secret-gateway.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,6 +1311,56 @@ describe("resolveCommandSecretRefsViaGateway", () => {
13111311
});
13121312
});
13131313

1314+
it("accepts an inactive web ref after an incomplete gateway snapshot", async () => {
1315+
const restoreDeps = setGoogleWebSearchTargetDeps();
1316+
const webPath = "plugins.entries.google.config.webSearch.apiKey";
1317+
try {
1318+
callGateway.mockResolvedValueOnce({
1319+
assignments: [],
1320+
diagnostics: [],
1321+
});
1322+
const result = await resolveCommandSecretRefsViaGateway({
1323+
config: {
1324+
tools: {
1325+
web: {
1326+
search: {
1327+
enabled: false,
1328+
provider: "gemini",
1329+
},
1330+
},
1331+
},
1332+
plugins: {
1333+
entries: {
1334+
google: {
1335+
config: {
1336+
webSearch: {
1337+
apiKey: {
1338+
source: "env",
1339+
provider: "default",
1340+
id: "missing-disabled-web-ref",
1341+
},
1342+
},
1343+
},
1344+
},
1345+
},
1346+
},
1347+
} as OpenClawConfig,
1348+
commandName: "agent",
1349+
targetIds: new Set([webPath]),
1350+
});
1351+
1352+
expect(result.hadUnresolvedTargets).toBe(false);
1353+
expect(result.targetStatesByPath[webPath]).toBe("inactive_surface");
1354+
expect(
1355+
result.diagnostics.some((entry) =>
1356+
entry.includes(`${webPath}: tools.web.search is disabled.`),
1357+
),
1358+
).toBe(true);
1359+
} finally {
1360+
restoreDeps();
1361+
}
1362+
});
1363+
13141364
it("limits strict local fallback analysis to unresolved gateway paths", async () => {
13151365
const locallyRecoveredKey = "TALK_API_KEY_PARTIAL_GATEWAY_LOCAL";
13161366
await withEnvValue(locallyRecoveredKey, "recovered-locally", async () => {

src/cli/command-secret-gateway.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,25 +1119,31 @@ export async function resolveCommandSecretRefsViaGateway(params: {
11191119
optionalActivePaths: params.optionalActivePaths,
11201120
resolutionPolicy,
11211121
});
1122+
const handledPaths = new Set<string>();
1123+
const locallyResolvedPaths = new Set<string>();
11221124
for (const unresolved of analyzed.unresolved) {
1123-
if (localFallback.targetStatesByPath[unresolved.path] !== "resolved_local") {
1125+
const localState = localFallback.targetStatesByPath[unresolved.path];
1126+
if (localState === "inactive_surface") {
1127+
// A partial gateway snapshot can omit inactive refs as well as unresolved refs.
1128+
// Local inactive classification is terminal even though it materializes no value.
1129+
targetStatesByPath[unresolved.path] = localState;
1130+
handledPaths.add(unresolved.path);
1131+
continue;
1132+
}
1133+
if (localState !== "resolved_local") {
11241134
continue;
11251135
}
11261136
setPathExistingStrict(
11271137
resolvedConfig,
11281138
unresolved.pathSegments,
11291139
getPath(localFallback.resolvedConfig, unresolved.pathSegments),
11301140
);
1131-
targetStatesByPath[unresolved.path] = "resolved_local";
1141+
targetStatesByPath[unresolved.path] = localState;
1142+
handledPaths.add(unresolved.path);
1143+
locallyResolvedPaths.add(unresolved.path);
11321144
}
1133-
const recoveredPaths = new Set(
1134-
Object.entries(localFallback.targetStatesByPath)
1135-
.filter(([, state]) => state === "resolved_local")
1136-
.map(([path]) => path),
1137-
);
1138-
const stillUnresolved = analyzed.unresolved.filter(
1139-
(entry) => !recoveredPaths.has(entry.path),
1140-
);
1145+
diagnostics = dedupeDiagnostics([...diagnostics, ...localFallback.diagnostics]);
1146+
const stillUnresolved = analyzed.unresolved.filter((entry) => !handledPaths.has(entry.path));
11411147
if (stillUnresolved.length > 0) {
11421148
if (enforcesResolvedSecrets(mode)) {
11431149
throw new Error(
@@ -1149,17 +1155,16 @@ export async function resolveCommandSecretRefsViaGateway(params: {
11491155
}
11501156
diagnostics = dedupeDiagnostics([
11511157
...diagnostics,
1152-
...localFallback.diagnostics,
11531158
...buildUnresolvedDiagnostics(params.commandName, stillUnresolved, mode),
11541159
]);
11551160
for (const unresolved of stillUnresolved) {
11561161
targetStatesByPath[unresolved.path] = "unresolved";
11571162
}
1158-
} else if (recoveredPaths.size > 0) {
1163+
} else if (locallyResolvedPaths.size > 0) {
11591164
diagnostics = dedupeDiagnostics([
11601165
...diagnostics,
1161-
`${params.commandName}: resolved ${recoveredPaths.size} secret ${
1162-
recoveredPaths.size === 1 ? "path" : "paths"
1166+
`${params.commandName}: resolved ${locallyResolvedPaths.size} secret ${
1167+
locallyResolvedPaths.size === 1 ? "path" : "paths"
11631168
} locally after the gateway snapshot was incomplete.`,
11641169
]);
11651170
}

0 commit comments

Comments
 (0)