mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 16:27:18 +02:00
Fix nested adapter config mediation
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
3e8c37a82e
commit
bc3a17b109
5 files changed
+344
-53
No files matched your search
@@ -54,7 +54,7 @@ export const createAgentInstructionsBundleSchema = z.object({
|
||||
const agentModelProfileConfigSchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
label: z.string().trim().min(1).optional(),
|
||||
adapterConfig: z.record(z.unknown()),
|
||||
adapterConfig: adapterConfigSchema,
|
||||
}).strict();
|
||||
|
||||
export const agentRuntimeConfigSchema = z.object({
|
||||
|
||||
@@ -140,6 +140,26 @@ describe("issue validators", () => {
|
||||
expect(parsed.runtimeConfig.heartbeat).toEqual({ enabled: true });
|
||||
});
|
||||
|
||||
it("validates cheap model profile env bindings like top-level adapter config", () => {
|
||||
const parsed = createAgentSchema.safeParse({
|
||||
name: "Coder",
|
||||
adapterType: "codex_local",
|
||||
runtimeConfig: {
|
||||
modelProfiles: {
|
||||
cheap: {
|
||||
adapterConfig: {
|
||||
env: {
|
||||
API_TOKEN: 123,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects unknown agent runtime model profile keys", () => {
|
||||
const parsed = createAgentSchema.safeParse({
|
||||
name: "Coder",
|
||||
|
||||
@@ -496,6 +496,165 @@ describe.sequential("agent permission routes", () => {
|
||||
expect(mockLogActivity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("blocks agent-authenticated self-updates that set cheap-profile host-executed workspace commands", async () => {
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "codex_local",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "agent",
|
||||
agentId,
|
||||
companyId,
|
||||
source: "agent_key",
|
||||
runId: "run-1",
|
||||
});
|
||||
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.patch(`/api/agents/${agentId}`)
|
||||
.send({
|
||||
runtimeConfig: {
|
||||
modelProfiles: {
|
||||
cheap: {
|
||||
adapterConfig: {
|
||||
workspaceStrategy: {
|
||||
type: "git_worktree",
|
||||
provisionCommand: "touch /tmp/paperclip-rce",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("host-executed workspace commands");
|
||||
expect(res.body.error).toContain(
|
||||
"runtimeConfig.modelProfiles.cheap.adapterConfig.workspaceStrategy.provisionCommand",
|
||||
);
|
||||
expect(mockLogActivity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows board updates that set cheap-profile workspace commands", async () => {
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "codex_local",
|
||||
});
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const runtimeConfig = {
|
||||
modelProfiles: {
|
||||
cheap: {
|
||||
adapterConfig: {
|
||||
workspaceStrategy: {
|
||||
type: "git_worktree",
|
||||
provisionCommand: "bash ./scripts/provision-worktree.sh",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.patch(`/api/agents/${agentId}`)
|
||||
.send({ runtimeConfig }));
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(200);
|
||||
expect(mockAgentService.update).toHaveBeenCalledWith(
|
||||
agentId,
|
||||
expect.objectContaining({ runtimeConfig }),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(mockLogActivity).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
|
||||
action: "agent.updated",
|
||||
}));
|
||||
});
|
||||
|
||||
it("normalizes cheap-profile env bindings through the adapter config secret pipeline", async () => {
|
||||
mockAgentService.getById.mockResolvedValue({
|
||||
...baseAgent,
|
||||
adapterType: "codex_local",
|
||||
});
|
||||
mockSecretService.normalizeAdapterConfigForPersistence.mockImplementation(async (_companyId, config) => ({
|
||||
...config,
|
||||
env: {
|
||||
API_TOKEN: {
|
||||
type: "secret_ref",
|
||||
secretId: "33333333-3333-4333-8333-333333333333",
|
||||
version: "latest",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const app = await createApp({
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
source: "local_implicit",
|
||||
isInstanceAdmin: true,
|
||||
companyIds: [companyId],
|
||||
});
|
||||
|
||||
const res = await requestApp(app, (baseUrl) => request(baseUrl)
|
||||
.patch(`/api/agents/${agentId}`)
|
||||
.send({
|
||||
runtimeConfig: {
|
||||
modelProfiles: {
|
||||
cheap: {
|
||||
adapterConfig: {
|
||||
model: "gpt-5.3-codex-spark",
|
||||
env: {
|
||||
API_TOKEN: {
|
||||
type: "secret_ref",
|
||||
secretId: "33333333-3333-4333-8333-333333333333",
|
||||
version: "latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(200);
|
||||
expect(mockSecretService.normalizeAdapterConfigForPersistence).toHaveBeenCalledWith(
|
||||
companyId,
|
||||
expect.objectContaining({
|
||||
model: "gpt-5.3-codex-spark",
|
||||
env: expect.any(Object),
|
||||
}),
|
||||
{ strictMode: false },
|
||||
);
|
||||
expect(mockAgentService.update).toHaveBeenCalledWith(
|
||||
agentId,
|
||||
expect.objectContaining({
|
||||
runtimeConfig: {
|
||||
modelProfiles: {
|
||||
cheap: {
|
||||
adapterConfig: {
|
||||
model: "gpt-5.3-codex-spark",
|
||||
env: {
|
||||
API_TOKEN: {
|
||||
type: "secret_ref",
|
||||
secretId: "33333333-3333-4333-8333-333333333333",
|
||||
version: "latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it("blocks agent-authenticated self-updates that set instructions bundle roots", async () => {
|
||||
const app = await createApp({
|
||||
type: "agent",
|
||||
|
||||
+159
-50
@@ -711,6 +711,99 @@ export function agentRoutes(
|
||||
return normalizedRuntimeConfig;
|
||||
}
|
||||
|
||||
function listRuntimeModelProfileAdapterConfigs(runtimeConfig: unknown): Array<{
|
||||
profileKey: string;
|
||||
profile: Record<string, unknown>;
|
||||
adapterConfig: Record<string, unknown>;
|
||||
path: string;
|
||||
}> {
|
||||
const runtimeRecord = asRecord(runtimeConfig);
|
||||
const modelProfiles = asRecord(runtimeRecord?.modelProfiles);
|
||||
if (!modelProfiles) return [];
|
||||
|
||||
const entries: Array<{
|
||||
profileKey: string;
|
||||
profile: Record<string, unknown>;
|
||||
adapterConfig: Record<string, unknown>;
|
||||
path: string;
|
||||
}> = [];
|
||||
for (const [profileKey, rawProfile] of Object.entries(modelProfiles)) {
|
||||
const profile = asRecord(rawProfile);
|
||||
const adapterConfig = asRecord(profile?.adapterConfig);
|
||||
if (!profile || !adapterConfig) continue;
|
||||
entries.push({
|
||||
profileKey,
|
||||
profile,
|
||||
adapterConfig,
|
||||
path: `runtimeConfig.modelProfiles.${profileKey}.adapterConfig`,
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function assertNoAgentRuntimeConfigAdapterConfigMutation(req: Request, runtimeConfig: unknown) {
|
||||
for (const entry of listRuntimeModelProfileAdapterConfigs(runtimeConfig)) {
|
||||
assertNoAgentAdapterConfigMutation(req, entry.adapterConfig, entry.path);
|
||||
}
|
||||
}
|
||||
|
||||
async function normalizeMediatedAdapterConfigForPersistence(input: {
|
||||
companyId: string;
|
||||
adapterType: string | null | undefined;
|
||||
adapterConfig: Record<string, unknown>;
|
||||
constraintAdapterConfig?: Record<string, unknown>;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(
|
||||
input.companyId,
|
||||
input.adapterConfig,
|
||||
{ strictMode: strictSecretsMode },
|
||||
);
|
||||
await assertAdapterConfigConstraints(
|
||||
input.companyId,
|
||||
input.adapterType,
|
||||
input.constraintAdapterConfig
|
||||
? { ...input.constraintAdapterConfig, ...normalizedAdapterConfig }
|
||||
: normalizedAdapterConfig,
|
||||
);
|
||||
return normalizedAdapterConfig;
|
||||
}
|
||||
|
||||
async function normalizeRuntimeConfigAdapterConfigsForPersistence(
|
||||
companyId: string,
|
||||
adapterType: string,
|
||||
runtimeConfig: Record<string, unknown>,
|
||||
baseAdapterConfig: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const entries = listRuntimeModelProfileAdapterConfigs(runtimeConfig);
|
||||
if (entries.length === 0) return runtimeConfig;
|
||||
const adapterModelProfiles = await listAdapterModelProfiles(adapterType);
|
||||
|
||||
const normalizedRuntimeConfig = { ...runtimeConfig };
|
||||
const modelProfiles = asRecord(runtimeConfig.modelProfiles) ?? {};
|
||||
const normalizedModelProfiles = { ...modelProfiles };
|
||||
normalizedRuntimeConfig.modelProfiles = normalizedModelProfiles;
|
||||
|
||||
for (const entry of entries) {
|
||||
const adapterProfile = adapterModelProfiles.find((profile) => profile.key === entry.profileKey);
|
||||
const adapterDefaultConfig = asRecord(adapterProfile?.adapterConfig) ?? {};
|
||||
const normalizedAdapterConfig = await normalizeMediatedAdapterConfigForPersistence({
|
||||
companyId,
|
||||
adapterType,
|
||||
adapterConfig: entry.adapterConfig,
|
||||
constraintAdapterConfig: {
|
||||
...baseAdapterConfig,
|
||||
...adapterDefaultConfig,
|
||||
},
|
||||
});
|
||||
normalizedModelProfiles[entry.profileKey] = {
|
||||
...entry.profile,
|
||||
adapterConfig: normalizedAdapterConfig,
|
||||
};
|
||||
}
|
||||
|
||||
return normalizedRuntimeConfig;
|
||||
}
|
||||
|
||||
function generateEd25519PrivateKeyPem(): string {
|
||||
const { privateKey } = generateKeyPairSync("ed25519");
|
||||
return privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
||||
@@ -867,15 +960,34 @@ export function agentRoutes(
|
||||
function assertNoAgentInstructionsConfigMutation(
|
||||
req: Request,
|
||||
adapterConfig: Record<string, unknown> | null | undefined,
|
||||
path = "adapterConfig",
|
||||
) {
|
||||
if (req.actor.type !== "agent" || !adapterConfig) return;
|
||||
const changedSensitiveKeys = KNOWN_INSTRUCTIONS_BUNDLE_KEYS.filter((key) => adapterConfig[key] !== undefined);
|
||||
const changedSensitiveKeys = KNOWN_INSTRUCTIONS_BUNDLE_KEYS
|
||||
.filter((key) => adapterConfig[key] !== undefined)
|
||||
.map((key) => `${path}.${key}`);
|
||||
if (changedSensitiveKeys.length === 0) return;
|
||||
throw forbidden(
|
||||
`Agent-authenticated callers cannot modify instructions path or bundle configuration (${changedSensitiveKeys.join(", ")})`,
|
||||
);
|
||||
}
|
||||
|
||||
function adapterConfigTouchesInstructionsConfig(adapterConfig: Record<string, unknown>) {
|
||||
return KNOWN_INSTRUCTIONS_BUNDLE_KEYS.some((key) => adapterConfig[key] !== undefined);
|
||||
}
|
||||
|
||||
function assertNoAgentAdapterConfigMutation(
|
||||
req: Request,
|
||||
adapterConfig: Record<string, unknown>,
|
||||
path = "adapterConfig",
|
||||
) {
|
||||
assertNoAgentInstructionsConfigMutation(req, adapterConfig, path);
|
||||
assertNoAgentHostWorkspaceCommandMutation(
|
||||
req,
|
||||
collectAgentAdapterWorkspaceCommandPaths(adapterConfig, path),
|
||||
);
|
||||
}
|
||||
|
||||
function summarizeAgentUpdateDetails(patch: Record<string, unknown>) {
|
||||
const changedTopLevelKeys = Object.keys(patch).sort();
|
||||
const details: Record<string, unknown> = { changedTopLevelKeys };
|
||||
@@ -1633,21 +1745,16 @@ export function agentRoutes(
|
||||
...hireInput
|
||||
} = req.body;
|
||||
hireInput.adapterType = assertKnownAdapterType(hireInput.adapterType);
|
||||
const rawHireAdapterConfig = (hireInput.adapterConfig ?? {}) as Record<string, unknown>;
|
||||
assertNoNewAgentLegacyPromptTemplate(
|
||||
hireInput.adapterType,
|
||||
(hireInput.adapterConfig ?? {}) as Record<string, unknown>,
|
||||
);
|
||||
assertNoAgentHostWorkspaceCommandMutation(
|
||||
req,
|
||||
collectAgentAdapterWorkspaceCommandPaths(hireInput.adapterConfig),
|
||||
);
|
||||
assertNoAgentInstructionsConfigMutation(
|
||||
req,
|
||||
(hireInput.adapterConfig ?? {}) as Record<string, unknown>,
|
||||
rawHireAdapterConfig,
|
||||
);
|
||||
assertNoAgentAdapterConfigMutation(req, rawHireAdapterConfig);
|
||||
assertNoAgentRuntimeConfigAdapterConfigMutation(req, hireInput.runtimeConfig);
|
||||
const requestedAdapterConfig = applyCreateDefaultsByAdapterType(
|
||||
hireInput.adapterType,
|
||||
((hireInput.adapterConfig ?? {}) as Record<string, unknown>),
|
||||
rawHireAdapterConfig,
|
||||
);
|
||||
const desiredSkillAssignment = await resolveDesiredSkillAssignment(
|
||||
companyId,
|
||||
@@ -1655,20 +1762,21 @@ export function agentRoutes(
|
||||
requestedAdapterConfig,
|
||||
Array.isArray(requestedDesiredSkills) ? requestedDesiredSkills : undefined,
|
||||
);
|
||||
const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(
|
||||
const normalizedAdapterConfig = await normalizeMediatedAdapterConfigForPersistence({
|
||||
companyId,
|
||||
desiredSkillAssignment.adapterConfig,
|
||||
{ strictMode: strictSecretsMode },
|
||||
);
|
||||
await assertAdapterConfigConstraints(
|
||||
adapterType: hireInput.adapterType,
|
||||
adapterConfig: desiredSkillAssignment.adapterConfig,
|
||||
});
|
||||
const normalizedRuntimeConfig = await normalizeRuntimeConfigAdapterConfigsForPersistence(
|
||||
companyId,
|
||||
hireInput.adapterType,
|
||||
normalizeNewAgentRuntimeConfig(hireInput.runtimeConfig),
|
||||
normalizedAdapterConfig,
|
||||
);
|
||||
const normalizedHireInput = {
|
||||
...hireInput,
|
||||
adapterConfig: normalizedAdapterConfig,
|
||||
runtimeConfig: normalizeNewAgentRuntimeConfig(hireInput.runtimeConfig),
|
||||
runtimeConfig: normalizedRuntimeConfig,
|
||||
};
|
||||
|
||||
const company = await db
|
||||
@@ -1823,21 +1931,16 @@ export function agentRoutes(
|
||||
...createInput
|
||||
} = req.body;
|
||||
createInput.adapterType = assertKnownAdapterType(createInput.adapterType);
|
||||
const rawCreateAdapterConfig = (createInput.adapterConfig ?? {}) as Record<string, unknown>;
|
||||
assertNoNewAgentLegacyPromptTemplate(
|
||||
createInput.adapterType,
|
||||
(createInput.adapterConfig ?? {}) as Record<string, unknown>,
|
||||
);
|
||||
assertNoAgentHostWorkspaceCommandMutation(
|
||||
req,
|
||||
collectAgentAdapterWorkspaceCommandPaths(createInput.adapterConfig),
|
||||
);
|
||||
assertNoAgentInstructionsConfigMutation(
|
||||
req,
|
||||
(createInput.adapterConfig ?? {}) as Record<string, unknown>,
|
||||
rawCreateAdapterConfig,
|
||||
);
|
||||
assertNoAgentAdapterConfigMutation(req, rawCreateAdapterConfig);
|
||||
assertNoAgentRuntimeConfigAdapterConfigMutation(req, createInput.runtimeConfig);
|
||||
const requestedAdapterConfig = applyCreateDefaultsByAdapterType(
|
||||
createInput.adapterType,
|
||||
((createInput.adapterConfig ?? {}) as Record<string, unknown>),
|
||||
rawCreateAdapterConfig,
|
||||
);
|
||||
const desiredSkillAssignment = await resolveDesiredSkillAssignment(
|
||||
companyId,
|
||||
@@ -1845,14 +1948,15 @@ export function agentRoutes(
|
||||
requestedAdapterConfig,
|
||||
Array.isArray(requestedDesiredSkills) ? requestedDesiredSkills : undefined,
|
||||
);
|
||||
const normalizedAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(
|
||||
const normalizedAdapterConfig = await normalizeMediatedAdapterConfigForPersistence({
|
||||
companyId,
|
||||
desiredSkillAssignment.adapterConfig,
|
||||
{ strictMode: strictSecretsMode },
|
||||
);
|
||||
await assertAdapterConfigConstraints(
|
||||
adapterType: createInput.adapterType,
|
||||
adapterConfig: desiredSkillAssignment.adapterConfig,
|
||||
});
|
||||
const normalizedRuntimeConfig = await normalizeRuntimeConfigAdapterConfigsForPersistence(
|
||||
companyId,
|
||||
createInput.adapterType,
|
||||
normalizeNewAgentRuntimeConfig(createInput.runtimeConfig),
|
||||
normalizedAdapterConfig,
|
||||
);
|
||||
await assertAgentEnvironmentSelection(companyId, createInput.adapterType, createInput.defaultEnvironmentId);
|
||||
@@ -1864,7 +1968,7 @@ export function agentRoutes(
|
||||
const createdAgent = await svc.create(companyId, {
|
||||
...createInput,
|
||||
adapterConfig: normalizedAdapterConfig,
|
||||
runtimeConfig: normalizeNewAgentRuntimeConfig(createInput.runtimeConfig),
|
||||
runtimeConfig: normalizedRuntimeConfig,
|
||||
status: "idle",
|
||||
spentMonthlyCents: 0,
|
||||
lastHeartbeatAt: null,
|
||||
@@ -2239,14 +2343,8 @@ export function agentRoutes(
|
||||
res.status(422).json({ error: "adapterConfig must be an object" });
|
||||
return;
|
||||
}
|
||||
assertNoAgentInstructionsConfigMutation(req, adapterConfig);
|
||||
assertNoAgentHostWorkspaceCommandMutation(
|
||||
req,
|
||||
collectAgentAdapterWorkspaceCommandPaths(adapterConfig),
|
||||
);
|
||||
const changingInstructionsConfig = Object.keys(adapterConfig).some((key) =>
|
||||
KNOWN_INSTRUCTIONS_BUNDLE_KEYS.includes(key as (typeof KNOWN_INSTRUCTIONS_BUNDLE_KEYS)[number]),
|
||||
);
|
||||
assertNoAgentAdapterConfigMutation(req, adapterConfig);
|
||||
const changingInstructionsConfig = adapterConfigTouchesInstructionsConfig(adapterConfig);
|
||||
if (changingInstructionsConfig) {
|
||||
await assertCanManageInstructionsPath(req, existing);
|
||||
}
|
||||
@@ -2256,6 +2354,16 @@ export function agentRoutes(
|
||||
const requestedAdapterType = hasOwn(patchData, "adapterType")
|
||||
? assertKnownAdapterType(patchData.adapterType as string | null | undefined)
|
||||
: existing.adapterType;
|
||||
let requestedRuntimeConfig: Record<string, unknown> | null = null;
|
||||
if (hasOwn(patchData, "runtimeConfig")) {
|
||||
const runtimeConfig = asRecord(patchData.runtimeConfig);
|
||||
if (!runtimeConfig) {
|
||||
res.status(422).json({ error: "runtimeConfig must be an object" });
|
||||
return;
|
||||
}
|
||||
assertNoAgentRuntimeConfigAdapterConfigMutation(req, runtimeConfig);
|
||||
requestedRuntimeConfig = runtimeConfig;
|
||||
}
|
||||
const touchesAdapterConfiguration =
|
||||
hasOwn(patchData, "adapterType") ||
|
||||
hasOwn(patchData, "adapterConfig");
|
||||
@@ -2301,19 +2409,20 @@ export function agentRoutes(
|
||||
requestedAdapterType,
|
||||
rawEffectiveAdapterConfig,
|
||||
);
|
||||
const normalizedEffectiveAdapterConfig = await secretsSvc.normalizeAdapterConfigForPersistence(
|
||||
existing.companyId,
|
||||
effectiveAdapterConfig,
|
||||
{ strictMode: strictSecretsMode },
|
||||
);
|
||||
const normalizedEffectiveAdapterConfig = await normalizeMediatedAdapterConfigForPersistence({
|
||||
companyId: existing.companyId,
|
||||
adapterType: requestedAdapterType,
|
||||
adapterConfig: effectiveAdapterConfig,
|
||||
});
|
||||
patchData.adapterConfig = syncInstructionsBundleConfigFromFilePath(existing, normalizedEffectiveAdapterConfig);
|
||||
}
|
||||
if (touchesAdapterConfiguration && requestedAdapterType === "opencode_local") {
|
||||
const effectiveAdapterConfig = asRecord(patchData.adapterConfig) ?? {};
|
||||
await assertAdapterConfigConstraints(
|
||||
if (requestedRuntimeConfig) {
|
||||
const baseAdapterConfig = asRecord(patchData.adapterConfig) ?? asRecord(existing.adapterConfig) ?? {};
|
||||
patchData.runtimeConfig = await normalizeRuntimeConfigAdapterConfigsForPersistence(
|
||||
existing.companyId,
|
||||
requestedAdapterType,
|
||||
effectiveAdapterConfig,
|
||||
requestedRuntimeConfig,
|
||||
baseAdapterConfig,
|
||||
);
|
||||
}
|
||||
if (touchesAdapterConfiguration || Object.prototype.hasOwnProperty.call(patchData, "defaultEnvironmentId")) {
|
||||
|
||||
@@ -47,11 +47,14 @@ export function assertNoAgentHostWorkspaceCommandMutation(req: Request, paths: s
|
||||
);
|
||||
}
|
||||
|
||||
export function collectAgentAdapterWorkspaceCommandPaths(adapterConfig: unknown): string[] {
|
||||
export function collectAgentAdapterWorkspaceCommandPaths(
|
||||
adapterConfig: unknown,
|
||||
prefix = "adapterConfig",
|
||||
): string[] {
|
||||
if (!isRecord(adapterConfig)) return [];
|
||||
return collectWorkspaceStrategyCommandPaths(
|
||||
adapterConfig.workspaceStrategy,
|
||||
"adapterConfig.workspaceStrategy",
|
||||
`${prefix}.workspaceStrategy`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user