diff --git a/config/agentrun.yaml b/config/agentrun.yaml index a6423d47..a38a965c 100644 --- a/config/agentrun.yaml +++ b/config/agentrun.yaml @@ -338,46 +338,24 @@ controlPlane: profile: codex - id: provider-gpt-pika-auth-json sourceMode: file - sourceRef: /root/.codex/auth.json + sourceRef: /root/.codex/auth.json.pika targetRef: namespace: agentrun-v02 name: agentrun-v02-provider-gpt-pika key: auth.json providerCredential: - profile: gpt.pika + sourceProfile: gpt.pika + profile: gpt-pika - id: provider-gpt-pika-config - sourceMode: codex-config - sourceRef: config/agentrun.yaml#controlPlane.lanes.nc01-v02.secrets.provider-gpt-pika-config.codexConfig - codexConfig: - modelProvider: OpenAI - model: gpt-5.4-mini - reviewModel: gpt-5.4-mini - modelReasoningEffort: xhigh - disableResponseStorage: true - networkAccess: enabled - windowsWslSetupAcknowledged: true - serviceTier: fast - modelProviders: - OpenAI: - name: OpenAI - baseUrl: https://api.pikapython.com/v1 - wireApi: responses - requiresOpenaiAuth: true - features: - goals: true - projects: - /root: - trustLevel: trusted - /root/unidesk: - trustLevel: trusted - tuiModelAvailabilityNux: - gpt-5.4-mini: 4 + sourceMode: file + sourceRef: /root/.codex/config.toml.pika targetRef: namespace: agentrun-v02 name: agentrun-v02-provider-gpt-pika key: config.toml providerCredential: - profile: gpt.pika + sourceProfile: gpt.pika + profile: gpt-pika - id: provider-dsflash-go-auth-json sourceMode: env sourceRef: hwlab/nc01-v03-code-agent-provider.env diff --git a/scripts/src/agentrun-lanes.ts b/scripts/src/agentrun-lanes.ts index 6953379d..de944105 100644 --- a/scripts/src/agentrun-lanes.ts +++ b/scripts/src/agentrun-lanes.ts @@ -107,6 +107,7 @@ export interface AgentRunLaneSecretSpec { readonly codexConfig: AgentRunCodexConfigSpec | null; readonly jsonValue: unknown; readonly targetRef: AgentRunSecretRef; + readonly providerCredentialSourceProfile: string | null; readonly providerCredentialProfile: string | null; } @@ -158,6 +159,7 @@ export interface AgentRunCodexProjectSpec { } export interface AgentRunProviderCredentialRef { + readonly sourceProfile: string; readonly profile: string; readonly secretRef: { readonly namespace: string; @@ -540,7 +542,10 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record ({ + sourceProfile: credential.sourceProfile, profile: credential.profile, secretRef: credential.secretRef, valuesPrinted: false, @@ -575,13 +581,15 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record(); - for (const secret of spec.secrets) { + const groups = new Map(); + for (const secret of agentRunProviderCredentialSecrets(spec)) { + const sourceProfile = secret.providerCredentialSourceProfile; const credentialProfile = secret.providerCredentialProfile; - if (credentialProfile === null) continue; + if (sourceProfile === null || credentialProfile === null) continue; if (profile !== undefined && profile !== null && credentialProfile !== profile) continue; - const groupKey = `${credentialProfile}\0${secret.targetRef.namespace}\0${secret.targetRef.name}`; + const groupKey = `${sourceProfile}\0${credentialProfile}\0${secret.targetRef.namespace}\0${secret.targetRef.name}`; const group = groups.get(groupKey) ?? { + sourceProfile, profile: credentialProfile, namespace: secret.targetRef.namespace, name: secret.targetRef.name, @@ -591,6 +599,7 @@ export function agentRunProviderCredentialRefs(spec: AgentRunLaneSpec, profile?: groups.set(groupKey, group); } return Array.from(groups.values()).map((group) => ({ + sourceProfile: group.sourceProfile, profile: group.profile, secretRef: { namespace: group.namespace, @@ -600,6 +609,25 @@ export function agentRunProviderCredentialRefs(spec: AgentRunLaneSpec, profile?: })); } +export function agentRunProviderCredentialRefsForSelection(spec: AgentRunLaneSpec, profile: string): readonly AgentRunProviderCredentialRef[] { + const effectiveProfiles = new Set( + agentRunProviderCredentialSecrets(spec, profile) + .map((secret) => secret.providerCredentialProfile) + .filter((value): value is string => value !== null), + ); + if (effectiveProfiles.size !== 1) return []; + return agentRunProviderCredentialRefs(spec, Array.from(effectiveProfiles)[0]); +} + +export function agentRunProviderCredentialSecrets(spec: AgentRunLaneSpec, profile?: string | null): readonly AgentRunLaneSecretSpec[] { + return spec.secrets.filter((secret) => { + const sourceProfile = secret.providerCredentialSourceProfile; + const effectiveProfile = secret.providerCredentialProfile; + if (sourceProfile === null || effectiveProfile === null) return false; + return profile === undefined || profile === null || profile === sourceProfile || profile === effectiveProfile; + }); +} + export function agentRunPipelineRunName(spec: AgentRunLaneSpec, sourceCommit: string): string { return `${spec.ci.pipelineRunPrefix}-${sourceCommit.slice(0, 12)}`; } @@ -1023,7 +1051,8 @@ function parseLaneSecret(input: Record, path: string): AgentRun codexConfig: sourceMode === "codex-config" ? parseCodexConfig(recordField(input, "codexConfig", path), `${path}.codexConfig`) : null, jsonValue, targetRef: parseNamespacedSecretRef(recordField(input, "targetRef", path), `${path}.targetRef`), - providerCredentialProfile: providerCredential, + providerCredentialSourceProfile: providerCredential?.sourceProfile ?? null, + providerCredentialProfile: providerCredential?.profile ?? null, }; } @@ -1031,6 +1060,8 @@ function parseLaneSecrets(input: Record, path: string): AgentRu const secrets = arrayField(input, "secrets", path).map((secret, index) => parseLaneSecret(secret, `${path}.secrets[${index}]`)); const idPaths = new Map(); const targetRefPaths = new Map(); + const sourceProfileMappings = new Map(); + const effectiveProfileMappings = new Map(); for (const [index, secret] of secrets.entries()) { const secretPath = `${path}.secrets[${index}]`; const previousIdPath = idPaths.get(secret.id); @@ -1044,6 +1075,20 @@ function parseLaneSecrets(input: Record, path: string): AgentRu throw new Error(`${secretPath}.targetRef duplicates ${previousTargetRefPath}.targetRef (${targetRef})`); } targetRefPaths.set(targetRef, secretPath); + if (secret.providerCredentialSourceProfile !== null && secret.providerCredentialProfile !== null) { + const sourceProfile = secret.providerCredentialSourceProfile; + const effectiveProfile = secret.providerCredentialProfile; + const priorEffective = sourceProfileMappings.get(sourceProfile); + if (priorEffective !== undefined && priorEffective.profile !== effectiveProfile) { + throw new Error(`${secretPath}.providerCredential maps sourceProfile ${sourceProfile} to ${effectiveProfile}, conflicting with ${priorEffective.path}.providerCredential.profile=${priorEffective.profile}`); + } + sourceProfileMappings.set(sourceProfile, { profile: effectiveProfile, path: secretPath }); + const priorSource = effectiveProfileMappings.get(effectiveProfile); + if (priorSource !== undefined && priorSource.sourceProfile !== sourceProfile) { + throw new Error(`${secretPath}.providerCredential maps effective profile ${effectiveProfile} from ${sourceProfile}, conflicting with ${priorSource.path}.providerCredential.sourceProfile=${priorSource.sourceProfile}`); + } + effectiveProfileMappings.set(effectiveProfile, { sourceProfile, path: secretPath }); + } } return secrets; } @@ -1207,13 +1252,15 @@ function integerRecord(value: unknown, path: string): Readonly, path: string): string | null { +function parseProviderCredentialBinding(input: Record, path: string): { sourceProfile: string; profile: string } | null { const value = input.providerCredential; if (value === undefined || value === null) return null; const record = asRecord(value, path); const profile = stringField(record, "profile", path); - validateSimpleId(profile, path); - return profile; + const sourceProfile = optionalStringField(record, "sourceProfile", path) ?? profile; + validateSimpleId(sourceProfile, `${path}.sourceProfile`); + validateAgentRunBackendProfile(profile, `${path}.profile`); + return { sourceProfile, profile }; } function parseGitMirrorRepositories(input: readonly Record[], path: string): readonly AgentRunGitMirrorRepositorySpec[] { @@ -1518,6 +1565,12 @@ function validateSimpleId(value: string, path: string): void { if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${path}.${value} must use a simple id`); } +function validateAgentRunBackendProfile(value: string, path: string): void { + if (!/^[a-z0-9][a-z0-9-]{0,63}$/u.test(value)) { + throw new Error(`${path} must match the canonical AgentRun backend profile slug /^[a-z0-9][a-z0-9-]{0,63}$/`); + } +} + function validateKubernetesNamePrefix(value: string, path: string): void { if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(value)) throw new Error(`${path} must be a Kubernetes name prefix`); } diff --git a/scripts/src/agentrun-secret-sync-output.test.ts b/scripts/src/agentrun-secret-sync-output.test.ts index d5abfc76..7adcee24 100644 --- a/scripts/src/agentrun-secret-sync-output.test.ts +++ b/scripts/src/agentrun-secret-sync-output.test.ts @@ -25,6 +25,8 @@ function blockedFixture(): Record { secret: index >= 11 ? "agentrun-v01-tool-github-ssh" : `fixture-target-${index}`, key: index === 11 ? "id_ed25519" : index === 12 ? "known_hosts" : `key-${index}`, sourceMode: "file", + sourceProfile: index === 0 ? "gpt.pika" : null, + effectiveBackendProfile: index === 0 ? "gpt-pika" : null, sourcePath: "/root/.unidesk/.state/secrets/", present: true, sourceFilePresent: true, @@ -162,6 +164,7 @@ describe("AgentRun secret-sync bounded output", () => { expect(json.plan.omittedCount).toBe(0); expect(json.plan.items.some((item: Record) => item.id === "tool-github-ssh-private-key")).toBe(true); expect(json.plan.items.some((item: Record) => item.id === "tool-github-ssh-known-hosts")).toBe(true); + expect(json.plan.items[0].profile).toEqual({ sourceProfile: "gpt.pika", effectiveBackendProfile: "gpt-pika" }); expect(json.activationFence).toMatchObject({ kind: "ProviderSecretActivationFence", identity: "providersecretactivationfence/NC01/nc01-v02", @@ -186,6 +189,8 @@ describe("AgentRun secret-sync bounded output", () => { expect(human).toContain("ACTIVATION FENCE"); expect(human).toContain("runnerjob/rjob_fixture"); expect(human).toContain("tool-github-ssh-private-key"); + expect(human).toContain("gpt.pika"); + expect(human).toContain("gpt-pika"); expect(human).toContain("VALUES PRINTED false"); for (const output of [human, jsonText, yamlText]) { expect(Buffer.byteLength(output, "utf8")).toBeLessThan(10_240); diff --git a/scripts/src/agentrun.test.ts b/scripts/src/agentrun.test.ts index 3b01c188..b81c2a71 100644 --- a/scripts/src/agentrun.test.ts +++ b/scripts/src/agentrun.test.ts @@ -1,7 +1,7 @@ import { readFileSync, rmSync } from "node:fs"; import { describe, expect, test } from "bun:test"; import { parseAgentRunClientConfigYaml, resolveAgentRunAuth, runAgentRunCommand, stripAgentRunResourceWrapperArgs } from "./agentrun"; -import { resolveAgentRunLaneTarget } from "./agentrun-lanes"; +import { agentRunProviderCredentialRefs, agentRunProviderCredentialRefsForSelection, resolveAgentRunLaneTarget } from "./agentrun-lanes"; import { agentRunToolCredentialRefs } from "./agentrun/config"; import { placeholderAgentRunImage, renderAgentRunGitopsFiles } from "./agentrun-manifests"; import { collectLaneSecretSources, secretSyncScript } from "./agentrun/secrets"; @@ -979,6 +979,60 @@ describe("AgentRun default transport contract", () => { }); describe("AgentRun YAML tool credential binding", () => { + test("maps the YAML source profile to one canonical AgentRun backend profile", async () => { + const { spec } = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" }); + const pikaSecrets = spec.secrets.filter((secret) => secret.providerCredentialSourceProfile === "gpt.pika"); + expect(pikaSecrets.map((secret) => ({ + id: secret.id, + sourceMode: secret.sourceMode, + sourceRef: secret.sourceRef, + sourceProfile: secret.providerCredentialSourceProfile, + effectiveBackendProfile: secret.providerCredentialProfile, + targetRef: secret.targetRef, + }))).toEqual([ + { + id: "provider-gpt-pika-auth-json", + sourceMode: "file", + sourceRef: "/root/.codex/auth.json.pika", + sourceProfile: "gpt.pika", + effectiveBackendProfile: "gpt-pika", + targetRef: { namespace: "agentrun-v02", name: "agentrun-v02-provider-gpt-pika", key: "auth.json" }, + }, + { + id: "provider-gpt-pika-config", + sourceMode: "file", + sourceRef: "/root/.codex/config.toml.pika", + sourceProfile: "gpt.pika", + effectiveBackendProfile: "gpt-pika", + targetRef: { namespace: "agentrun-v02", name: "agentrun-v02-provider-gpt-pika", key: "config.toml" }, + }, + ]); + const expected = [{ + sourceProfile: "gpt.pika", + profile: "gpt-pika", + secretRef: { + namespace: "agentrun-v02", + name: "agentrun-v02-provider-gpt-pika", + keys: ["auth.json", "config.toml"], + }, + }]; + expect(agentRunProviderCredentialRefsForSelection(spec, "gpt.pika")).toEqual(expected); + expect(agentRunProviderCredentialRefs(spec, "gpt-pika")).toEqual(expected); + expect(agentRunProviderCredentialRefs(spec, "gpt.pika")).toEqual([]); + + const invalidPath = `/tmp/unidesk-agentrun-invalid-profile-${process.pid}-${Date.now()}.yaml`; + try { + const invalid = readFileSync("config/agentrun.yaml", "utf8").replaceAll(" profile: gpt-pika", " profile: gpt.pika"); + await Bun.write(invalidPath, invalid); + expect(() => resolveAgentRunLaneTarget( + { node: "NC01", lane: "nc01-v02" }, + { ...process.env, AGENTRUN_CONTROL_PLANE_CONFIG: invalidPath }, + )).toThrow("canonical AgentRun backend profile slug"); + } finally { + rmSync(invalidPath, { force: true }); + } + }); + test("aggregates managed GitHub SSH source keys into one volume credential and one sync target", () => { const { spec } = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" }); const githubSsh = agentRunToolCredentialRefs(spec).filter((credential) => credential.tool === "github" && credential.purpose === "github-ssh"); @@ -1020,6 +1074,290 @@ describe("AgentRun YAML tool credential binding", () => { expect(JSON.stringify({ githubSsh, sources })).not.toContain("/root/.ssh"); }); + test("shares model overrides across Aipod render, create, and send without overriding provider authority", async () => { + const renderBodies: Record[] = []; + const submittedBodies: Record[] = []; + const sendBodies: Record[] = []; + let renderedBackendProfile = "gpt-pika"; + let renderedCredentialProfile = "gpt-pika"; + let renderedSecretName = "agentrun-v02-provider-gpt-pika"; + let resolutionModelOverride: string | null = null; + const server = Bun.serve({ + port: 0, + async fetch(request) { + const url = new URL(request.url); + expect(request.headers.get("authorization")).toBe("Bearer secret-value"); + if (url.pathname === "/api/v1/aipod-specs/Artificer/render") { + const input = await request.json() as Record; + renderBodies.push(input); + const model = input.model ?? "gpt-5.6-terra"; + const reasoningEffort = input.reasoningEffort ?? "xhigh"; + const modelSource = input.model === undefined ? "aipod-spec-default" : "render-input-override"; + const reasoningEffortSource = input.reasoningEffort === undefined ? "aipod-spec-default" : "render-input-override"; + return Response.json({ + ok: true, + data: { + queueTask: { + tenantId: "unidesk", + projectId: "pikasTech/unidesk", + queue: "commander", + lane: "v0.2", + providerId: "NC01", + backendProfile: renderedBackendProfile, + workspaceRef: input.workspaceRef ?? { kind: "opaque", path: "." }, + executionPolicy: { + sandbox: "workspace-write", + approval: "never", + timeoutMs: 1_800_000, + network: "enabled", + secretScope: { + allowCredentialEcho: false, + providerCredentials: [{ + profile: renderedCredentialProfile, + secretRef: { + namespace: "agentrun-v02", + name: renderedSecretName, + keys: ["auth.json", "config.toml"], + }, + }], + }, + }, + payload: { + prompt: input.prompt ?? "fixture", + model, + reasoningEffort, + modelConfig: { model, reasoningEffort, modelSource, reasoningEffortSource, valuesPrinted: false }, + }, + }, + dispatchDefaults: { + runnerJob: { namespace: "agentrun-v02" }, + }, + modelResolution: { + model: resolutionModelOverride ?? model, + reasoningEffort, + modelSource, + reasoningEffortSource, + modelOverridden: input.model !== undefined, + reasoningEffortOverridden: input.reasoningEffort !== undefined, + backendProfile: "gpt-pika", + backendProfileSource: "aipod-spec", + providerCredential: { + profile: "gpt-pika", + secretRef: { + namespace: "agentrun-v02", + name: "agentrun-v02-provider-gpt-pika", + keys: ["auth.json", "config.toml"], + valuesPrinted: false, + }, + valuesPrinted: false, + }, + valuesPrinted: false, + }, + }, + }); + } + if (url.pathname === "/api/v1/queue/tasks") { + submittedBodies.push(await request.json() as Record); + return Response.json({ ok: true, data: { id: "qt_gpt_pika", state: "pending" } }); + } + if (url.pathname === "/api/v1/sessions/ses_gpt_pika/send") { + const body = await request.json() as Record; + sendBodies.push(body); + return Response.json({ + ok: true, + data: { + action: "session-send-plan", + dryRun: body.dryRun, + mutation: false, + sessionId: "ses_gpt_pika", + decision: "turn", + internalCommandType: "turn", + request: { method: "POST", path: url.pathname, createRunnerJob: true, valuesPrinted: false }, + valuesPrinted: false, + }, + }); + } + return new Response("not found", { status: 404 }); + }, + }); + const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG; + const previousKey = process.env.HWLAB_API_KEY; + process.env.HWLAB_API_KEY = "secret-value"; + const tempConfigPath = `/tmp/unidesk-agentrun-gpt-pika-${process.pid}-${Date.now()}.yaml`; + const tempInputPath = `/tmp/unidesk-agentrun-gpt-pika-input-${process.pid}-${Date.now()}.json`; + try { + process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath; + await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, ""))); + + const defaultPlan = JSON.parse(renderedTextOf(await runAgentRunCommand(null, [ + "create", "task", "--aipod", "Artificer", "--prompt", "default fixture", "--dry-run", "-o", "json", + ]))) as Record; + expect(defaultPlan.aipodBinding).toMatchObject({ + sourceProfile: "gpt.pika", + effectiveBackendProfile: "gpt-pika", + backendProfileSource: "aipod-spec", + model: "gpt-5.6-terra", + reasoningEffort: "xhigh", + modelSource: "aipod-spec-default", + reasoningEffortSource: "aipod-spec-default", + modelOverridden: false, + reasoningEffortOverridden: false, + valuesPrinted: false, + }); + expect(defaultPlan.aipodBinding.providerCredentials).toEqual([{ + profile: "gpt-pika", + secretRef: { name: "agentrun-v02-provider-gpt-pika", keys: ["auth.json", "config.toml"] }, + valuesPrinted: false, + }]); + expect(renderBodies[0]).toEqual({ prompt: "default fixture" }); + + const override = await runAgentRunCommand(null, [ + "create", "task", "--aipod", "Artificer", "--prompt", "override fixture", + "--model", "gpt-5.6-terra-alt", "--reasoning-effort", "xhigh", "-o", "json", + ]); + expect(override.ok).toBe(true); + expect(renderBodies[1]).toMatchObject({ + prompt: "override fixture", + model: "gpt-5.6-terra-alt", + reasoningEffort: "xhigh", + }); + expect(renderBodies[1]?.backendProfile).toBeUndefined(); + expect(renderBodies[1]?.executionPolicy).toBeUndefined(); + expect(submittedBodies[0]).toMatchObject({ + backendProfile: "gpt-pika", + payload: { model: "gpt-5.6-terra-alt", reasoningEffort: "xhigh" }, + executionPolicy: { + secretScope: { + providerCredentials: [{ + profile: "gpt-pika", + secretRef: { name: "agentrun-v02-provider-gpt-pika", keys: ["auth.json", "config.toml"] }, + }], + }, + }, + }); + + const send = await runAgentRunCommand(null, [ + "send", "session/ses_gpt_pika", "--aipod", "Artificer", "--prompt", "send fixture", + "--model", "gpt-5.6-terra-send", "--reasoning-effort", "medium", "--dry-run", "-o", "json", + ]); + expect(send.ok).toBe(true); + const sendProjection = JSON.parse(renderedTextOf(send)) as Record; + expect(sendProjection.aipodBinding).toMatchObject({ + sourceProfile: "gpt.pika", + effectiveBackendProfile: "gpt-pika", + model: "gpt-5.6-terra-send", + reasoningEffort: "medium", + modelSource: "render-input-override", + reasoningEffortSource: "render-input-override", + modelOverridden: true, + reasoningEffortOverridden: true, + valuesPrinted: false, + }); + expect(renderBodies[2]).toMatchObject({ model: "gpt-5.6-terra-send", reasoningEffort: "medium" }); + expect(sendBodies[0]).toMatchObject({ + dryRun: true, + run: { backendProfile: "gpt-pika" }, + payload: { model: "gpt-5.6-terra-send", reasoningEffort: "medium" }, + }); + + const render = await runAgentRunCommand(null, [ + "aipod-specs", "render", "Artificer", "--model", "gpt-5.6-terra-render", "--reasoning-effort", "low", "-o", "json", + ]); + expect(render.ok).toBe(true); + expect(renderBodies[3]).toEqual({ model: "gpt-5.6-terra-render", reasoningEffort: "low" }); + + const createHuman = renderedTextOf(await runAgentRunCommand(null, [ + "create", "task", "--aipod", "Artificer", "--prompt", "human create", "--dry-run", + ])); + const sendHuman = renderedTextOf(await runAgentRunCommand(null, [ + "send", "session/ses_gpt_pika", "--aipod", "Artificer", "--prompt", "human send", + "--model", "gpt-5.6-terra-human", "--reasoning-effort", "medium", "--dry-run", + ])); + const renderHuman = renderedTextOf(await runAgentRunCommand(null, [ + "aipod-specs", "render", "Artificer", "--model", "gpt-5.6-terra-human", "--reasoning-effort", "medium", + ])); + for (const human of [createHuman, sendHuman, renderHuman]) { + expect(human).toContain("sourceProfile=gpt.pika"); + expect(human).toContain("effectiveBackendProfile=gpt-pika"); + expect(human).toContain("model="); + expect(human).toContain("reasoningEffort="); + expect(human).toContain("valuesPrinted=false"); + expect(human).not.toContain("secret-value"); + } + expect(createHuman).toContain("modelSource=aipod-spec-default"); + expect(sendHuman).toContain("modelSource=render-input-override"); + expect(renderHuman).toContain("modelSource=render-input-override"); + + for (const helpArgs of [["create", "--help"], ["send", "--help"], ["aipod-specs", "render", "--help"]]) { + const help = renderedTextOf(await runAgentRunCommand(null, helpArgs)); + expect(help).toContain("--model "); + expect(help).toContain("--reasoning-effort "); + } + + resolutionModelOverride = "gpt-5.6-terra-conflict"; + const resolutionRejected = await runAgentRunCommand(null, [ + "create", "task", "--aipod", "Artificer", "--prompt", "resolution conflict", "-o", "json", + ]); + expect(resolutionRejected.ok).toBe(false); + expect(renderedTextOf(resolutionRejected)).toContain("queueTask.payload.model must match modelResolution.model"); + expect(submittedBodies).toHaveLength(1); + resolutionModelOverride = null; + + const requestCountBeforeRejections = renderBodies.length; + for (const forbidden of [ + ["--backend-profile", "gpt-pika"], + ["--profile", "gpt-pika"], + ["--execution-policy-json", "{}"], + ]) { + const rejected = await runAgentRunCommand(null, [ + "create", "task", "--aipod", "Artificer", "--prompt", "rejected", ...forbidden, "-o", "json", + ]); + expect(rejected.ok).toBe(false); + expect(renderedTextOf(rejected)).toContain("AipodSpec owns backendProfile"); + } + await Bun.write(tempInputPath, JSON.stringify({ payload: { prompt: "nested", model: "gpt-5.6-terra-nested" } })); + const nestedRejected = await runAgentRunCommand(null, [ + "create", "task", "--aipod", "Artificer", "--json-file", tempInputPath, "-o", "json", + ]); + expect(nestedRejected.ok).toBe(false); + expect(renderedTextOf(nestedRejected)).toContain("payload.model"); + await Bun.write(tempInputPath, JSON.stringify({ modelConfig: { model: "gpt-5.6-terra-nested" } })); + const modelConfigRejected = await runAgentRunCommand(null, [ + "create", "task", "--aipod", "Artificer", "--json-file", tempInputPath, "-o", "json", + ]); + expect(modelConfigRejected.ok).toBe(false); + expect(renderedTextOf(modelConfigRejected)).toContain("modelConfig"); + expect(renderBodies).toHaveLength(requestCountBeforeRejections); + + renderedBackendProfile = "gpt.pika"; + renderedCredentialProfile = "gpt.pika"; + const dottedRejected = await runAgentRunCommand(null, [ + "create", "task", "--aipod", "Artificer", "--prompt", "dotted", "-o", "json", + ]); + expect(dottedRejected.ok).toBe(false); + expect(renderedTextOf(dottedRejected)).toContain("backendProfile=gpt.pika"); + expect(submittedBodies).toHaveLength(1); + + renderedBackendProfile = "gpt-pika"; + renderedCredentialProfile = "gpt-pika"; + renderedSecretName = "agentrun-v01-provider-codex"; + const secretRejected = await runAgentRunCommand(null, [ + "create", "task", "--aipod", "Artificer", "--prompt", "wrong secret", "-o", "json", + ]); + expect(secretRejected.ok).toBe(false); + expect(renderedTextOf(secretRejected)).toContain("is not the unique YAML binding"); + expect(submittedBodies).toHaveLength(1); + } finally { + server.stop(true); + if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG; + else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig; + if (previousKey === undefined) delete process.env.HWLAB_API_KEY; + else process.env.HWLAB_API_KEY = previousKey; + rmSync(tempConfigPath, { force: true }); + rmSync(tempInputPath, { force: true }); + } + }); + test("preserves explicit credential subsets across Aipod admission and rejects unknown identities or conflicts", async () => { const renderBodies: Record[] = []; const submittedBodies: Record[] = []; @@ -1081,8 +1419,40 @@ describe("AgentRun YAML tool credential binding", () => { toolCredentials: renderedToolCredentials(), }, }, - payload: { prompt: "fixture" }, + payload: { + prompt: "fixture", + model: "gpt-5.5", + reasoningEffort: "high", + modelConfig: { + model: "gpt-5.5", + reasoningEffort: "high", + modelSource: "aipod-spec-default", + reasoningEffortSource: "aipod-spec-default", + valuesPrinted: false, + }, + }, }); + const modelResolution = { + model: "gpt-5.5", + reasoningEffort: "high", + modelSource: "aipod-spec-default", + reasoningEffortSource: "aipod-spec-default", + modelOverridden: false, + reasoningEffortOverridden: false, + backendProfile: "codex", + backendProfileSource: "aipod-spec", + providerCredential: { + profile: "codex", + secretRef: { + namespace: "agentrun-v02", + name: "agentrun-v01-provider-codex", + keys: ["auth.json", "config.toml"], + valuesPrinted: false, + }, + valuesPrinted: false, + }, + valuesPrinted: false, + }; const server = Bun.serve({ port: 0, async fetch(request) { @@ -1090,7 +1460,7 @@ describe("AgentRun YAML tool credential binding", () => { expect(request.headers.get("authorization")).toBe("Bearer secret-value"); if (url.pathname === "/api/v1/aipod-specs/Artificer/render") { renderBodies.push(await request.json() as Record); - return Response.json({ ok: true, data: { queueTask: renderedTask() } }); + return Response.json({ ok: true, data: { queueTask: renderedTask(), modelResolution } }); } if (url.pathname === "/api/v1/queue/tasks") { submittedBodies.push(await request.json() as Record); @@ -1111,20 +1481,20 @@ describe("AgentRun YAML tool credential binding", () => { expect(accepted.ok).toBe(true); expect(renderBodies).toHaveLength(1); expect(submittedBodies).toHaveLength(1); - expect(renderBodies[0]?.executionPolicy?.secretScope?.toolCredentials?.map((credential: Record) => `${credential.tool}/${credential.purpose}`)).toEqual([ + expect(renderBodies[0]?.backendProfile).toBeUndefined(); + expect(renderBodies[0]?.executionPolicy).toBeUndefined(); + expect(submittedBodies[0]?.executionPolicy?.secretScope?.toolCredentials?.map((credential: Record) => `${credential.tool}/${credential.purpose}`)).toEqual([ "github/github-pr", "unidesk-ssh/ssh-passthrough", "github/github-ssh", ]); - for (const body of [renderBodies[0], submittedBodies[0]]) { - const githubSsh = body?.executionPolicy?.secretScope?.toolCredentials?.filter((credential: Record) => credential.tool === "github" && credential.purpose === "github-ssh"); - expect(githubSsh).toEqual([{ - tool: "github", - purpose: "github-ssh", - secretRef: { name: "agentrun-v01-tool-github-ssh", keys: ["id_ed25519", "known_hosts"] }, - projection: { kind: "volume", mountPath: "/home/agentrun/.ssh" }, - }]); - } + const githubSsh = submittedBodies[0]?.executionPolicy?.secretScope?.toolCredentials?.filter((credential: Record) => credential.tool === "github" && credential.purpose === "github-ssh"); + expect(githubSsh).toEqual([{ + tool: "github", + purpose: "github-ssh", + secretRef: { name: "agentrun-v01-tool-github-ssh", keys: ["id_ed25519", "known_hosts"] }, + projection: { kind: "volume", mountPath: "/home/agentrun/.ssh" }, + }]); credentialMode = "subset"; const subsetAccepted = await runAgentRunCommand(null, ["create", "task", "--aipod", "Artificer", "--prompt", "fixture", "-o", "json"]); diff --git a/scripts/src/agentrun/config.ts b/scripts/src/agentrun/config.ts index 02ba35d1..8f357550 100644 --- a/scripts/src/agentrun/config.ts +++ b/scripts/src/agentrun/config.ts @@ -427,28 +427,45 @@ export function readYamlInputFromArgs(args: string[]): string { export async function aipodRenderInputFromArgs(args: string[], trailingPromptStart: number, overrides: Record = {}): Promise> { const input = await optionalJsonBody(args); - const prompt = optionalPromptFromArgs(args, trailingPromptStart); + assertAipodRenderProviderAuthority(args, input); + const prompt = optionalPromptFromArgs(args, args.length); if (prompt !== null) input.prompt = prompt; - copyAgentRunOptions(args, input, ["tenant-id", "project-id", "queue", "node", "lane", "title", "provider-id", "idempotency-key", "session-id"]); - const sessionPolicy = readAgentRunClientConfig().client.sessionPolicy; - const policyTarget = resolveAgentRunSessionPolicyTarget({ node: stringOrNull(input.node), lane: stringOrNull(input.lane) }); - if (input.node === undefined) input.node = policyTarget.spec.nodeId; - if (input.lane === undefined) input.lane = policyTarget.spec.lane; - if (input.providerId === undefined) input.providerId = defaultAgentRunProviderId(sessionPolicy, policyTarget); - const profile = agentRunOption(args, "profile") ?? agentRunOption(args, "backend-profile"); - const backendProfile = profile ?? stringOrNull(input.backendProfile) ?? sessionPolicy.backendProfile; - input.backendProfile = backendProfile; - if (!isRecord(input.workspaceRef)) input.workspaceRef = cloneJsonRecord(sessionPolicy.workspaceRef); - if (!isRecord(input.executionPolicy)) { - input.executionPolicy = defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy, policyTarget, { includeToolCredentials: true }); - } + copyAgentRunOptions(args, input, ["tenant-id", "project-id", "queue", "node", "lane", "title", "provider-id", "idempotency-key", "session-id", "model", "reasoning-effort"]); const priority = agentRunOption(args, "priority"); if (priority) input.priority = Number(priority); const workspaceRef = jsonObjectOption(args, "workspace-json"); if (workspaceRef !== null) input.workspaceRef = workspaceRef; + void trailingPromptStart; return { ...input, ...overrides }; } +function assertAipodRenderProviderAuthority(args: string[], input: Record): void { + const forbiddenFlags = ["profile", "backend-profile", "execution-policy-json"] + .filter((flag) => agentRunHasFlag(args, flag)) + .map((flag) => `--${flag}`); + if (forbiddenFlags.length > 0) { + throw new AgentRunRestError( + "validation-failed", + `AipodSpec owns backendProfile, provider credential SecretRef, and executionPolicy; remove ${forbiddenFlags.join(", ")} and use only --model/--reasoning-effort for task-level model selection`, + ); + } + const forbiddenTopLevel = ["profile", "backendProfile", "modelConfig", "executionPolicy", "providerCredentials", "providerCredential", "secretRef"] + .filter((key) => Object.hasOwn(input, key)); + const payload = record(input.payload); + const forbiddenPayload = ["model", "reasoningEffort", "modelConfig", "backendProfile", "executionPolicy", "providerCredentials", "providerCredential", "secretRef"] + .filter((key) => Object.hasOwn(payload, key)); + if (forbiddenTopLevel.length > 0 || forbiddenPayload.length > 0) { + const fields = [ + ...forbiddenTopLevel, + ...forbiddenPayload.map((key) => `payload.${key}`), + ]; + throw new AgentRunRestError( + "validation-failed", + `Aipod render input cannot override provider authority or place model overrides in payload: ${fields.join(", ")}; use top-level model/reasoningEffort`, + ); + } +} + export function readPromptFromArgs(args: string[], trailingStart: number): string { const prompt = optionalPromptFromArgs(args, trailingStart); if (prompt === null) throw new AgentRunRestError("validation-failed", "prompt is required; use --prompt-stdin, --prompt-file, --prompt, or a trailing prompt"); diff --git a/scripts/src/agentrun/entry.ts b/scripts/src/agentrun/entry.ts index 373d6e1f..777e90e5 100644 --- a/scripts/src/agentrun/entry.ts +++ b/scripts/src/agentrun/entry.ts @@ -68,9 +68,9 @@ export function agentRunHelp(): unknown { "bun scripts/cli.ts agentrun retry task/ --idempotency-key --reason --dry-run", "bun scripts/cli.ts agentrun get attempts --task --limit 20", "bun scripts/cli.ts agentrun dispatch task/", - "bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin --idempotency-key ", + "bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin --model --reasoning-effort --idempotency-key ", "bun scripts/cli.ts agentrun apply -f - --dry-run", - "bun scripts/cli.ts agentrun send session/ --aipod Artificer --prompt-stdin", + "bun scripts/cli.ts agentrun send session/ --aipod Artificer --prompt-stdin --model --reasoning-effort ", "bun scripts/cli.ts agentrun explain task", "bun scripts/cli.ts agentrun explain session-policy", "bun scripts/cli.ts agentrun control-plane plan --node NC01 --lane nc01-v02", @@ -316,13 +316,21 @@ export function agentRunHelpText(args: string[]): string { return "Usage: bun scripts/cli.ts agentrun dispatch task/"; } if (verb === "create") { - return "Usage: bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin [--idempotency-key ] [--dry-run]"; + return [ + "Usage: bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin [--model ] [--reasoning-effort ] [--idempotency-key ] [--dry-run]", + "", + "AipodSpec owns provider credentials and execution policy. Task-level model/reasoning overrides keep the YAML sourceProfile and canonical effectiveBackendProfile fixed.", + ].join("\n"); } if (verb === "apply") { return "Usage: bun scripts/cli.ts agentrun apply -f task.yaml|json|- [--dry-run]\nTask manifests use kind: Task and spec: ."; } if (verb === "send") { - return "Usage: bun scripts/cli.ts agentrun send session/ --aipod Artificer --prompt-stdin\nThe server decides whether this becomes an internal steer or a new turn from session state."; + return [ + "Usage: bun scripts/cli.ts agentrun send session/ --aipod Artificer --prompt-stdin [--model ] [--reasoning-effort ] [--dry-run]", + "", + "AipodSpec owns provider credentials and execution policy. The server decides whether this becomes an internal steer or a new turn from session state.", + ].join("\n"); } if (verb === "explain") return agentRunExplain(kind ?? "task"); if (verb === "control-plane") { @@ -382,6 +390,13 @@ export function agentRunHelpText(args: string[]): string { ].join("\n"); } if (verb !== undefined && isAgentRunRestCompatGroup(verb)) { + if ((verb === "aipod-specs" || verb === "aipods") && kind === "render") { + return [ + `Usage: bun scripts/cli.ts agentrun ${verb} render [--model ] [--reasoning-effort ] [-o json|yaml] [--full|--raw]`, + "", + "Human output shows sourceProfile, effectiveBackendProfile, model/reasoning resolution, and SecretRef-only provider binding with valuesPrinted=false.", + ].join("\n"); + } return [ `Compatibility group: agentrun ${verb} ...`, "", @@ -411,7 +426,7 @@ export function agentRunHelpText(args: string[]): string { " bun scripts/cli.ts agentrun logs session/ --tail 100", " bun scripts/cli.ts agentrun retry task/ --idempotency-key --reason --dry-run", " bun scripts/cli.ts agentrun get attempts --task --limit 20", - " bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin", + " bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin --model --reasoning-effort ", "", "Machine/debug output:", " -o json|yaml emits a stable UniDesk resource object without the global JSON envelope.", diff --git a/scripts/src/agentrun/provider-profile.ts b/scripts/src/agentrun/provider-profile.ts index 839ab8b8..85781c5f 100644 --- a/scripts/src/agentrun/provider-profile.ts +++ b/scripts/src/agentrun/provider-profile.ts @@ -11,6 +11,8 @@ import { import { agentRunLaneSummary, agentRunProviderCredentialRefs, + agentRunProviderCredentialRefsForSelection, + agentRunProviderCredentialSecrets, resolveAgentRunLaneTarget, type AgentRunLaneSecretSpec, type AgentRunLaneSpec, @@ -106,6 +108,9 @@ export function providerProfileHelp(): string { " bun scripts/cli.ts job status --tail-bytes 12000", " bun scripts/cli.ts agentrun provider-profile validate --node NC01 --lane nc01-v02 --profile gpt.pika --wait", "", + "Profile identity:", + " --profile accepts the YAML source selector or canonical AgentRun backend slug. Output always discloses sourceProfile and effectiveBackendProfile separately.", + "", "Boundary:", " confirmed apply returns an async job only after the provider Secret gate reports an exact data no-op; the worker skips the provider Secret write, publishes runtime GitOps using the current manager image, refreshes Argo and restarts the manager without rebuilding images or creating a PipelineRun.", ].join("\n"); @@ -128,7 +133,7 @@ async function providerProfileApplyAsync(config: UniDeskConfig, options: Provide const plan = providerProfilePlan({ ...options, confirm: false, dryRun: true }); if (plan.ok !== true) return plan; const { spec, profile } = resolveProviderProfile(options); - const secrets = spec.secrets.filter((secret) => secret.providerCredentialProfile === profile); + const secrets = [...agentRunProviderCredentialSecrets(spec, profile)]; const activationTargets = providerProfileActivationTargets(inspectProviderProfileSources(spec, secrets)); const activationPreflight = await providerSecretActivationPreflight(config, spec, activationTargets, { full: options.full || options.raw }); if (activationPreflight.ok !== true) { @@ -201,9 +206,9 @@ async function providerProfileRuntimeStatus(config: UniDeskConfig, options: Prov function resolveProviderProfile(options: ProviderProfileOptions): { configPath: string; spec: AgentRunLaneSpec; profile: string; secrets: AgentRunLaneSecretSpec[] } { if (options.profile === null) throw new Error("provider-profile requires --profile"); const target = resolveAgentRunLaneTarget(options); - const secrets = target.spec.secrets.filter((secret) => secret.providerCredentialProfile === options.profile); + const secrets = [...agentRunProviderCredentialSecrets(target.spec, options.profile)]; if (secrets.length === 0) { - const declared = agentRunProviderCredentialRefs(target.spec).map((item) => item.profile); + const declared = agentRunProviderCredentialRefs(target.spec).map((item) => `${item.sourceProfile}->${item.profile}`); throw new Error(`provider profile ${options.profile} is not declared in ${target.configPath}; declared profiles: ${declared.join(", ") || "(none)"}`); } return { ...target, profile: options.profile, secrets }; @@ -759,11 +764,11 @@ async function publishProfileGitops(config: UniDeskConfig, spec: AgentRunLaneSpe } function providerProfileSummary(spec: AgentRunLaneSpec, profile: string, secrets: readonly AgentRunLaneSecretSpec[]): Record { - const credential = agentRunProviderCredentialRefs(spec, profile)[0] ?? null; + const credential = agentRunProviderCredentialRefsForSelection(spec, profile)[0] ?? null; return { - profile, + sourceProfile: credential?.sourceProfile ?? profile, + effectiveBackendProfile: credential?.profile ?? null, declared: true, - backendProfile: profile === "gpt.pika" ? "gpt-pika" : profile, secretIds: secrets.map((secret) => secret.id), secretRef: credential?.secretRef ?? null, configuredKeys: credential?.secretRef.keys ?? secrets.map((secret) => secret.targetRef.key), diff --git a/scripts/src/agentrun/public-exposure.ts b/scripts/src/agentrun/public-exposure.ts index 7c3feb81..35ac1a24 100644 --- a/scripts/src/agentrun/public-exposure.ts +++ b/scripts/src/agentrun/public-exposure.ts @@ -278,8 +278,9 @@ export function renderAgentRunControlPlaneActionSummary(result: Record [ - displayValue(credential.profile), + renderTable(["SOURCE PROFILE", "BACKEND PROFILE", "NAMESPACE", "SECRET", "KEYS", "VALUES"], providerCredentials.slice(0, 8).map((credential) => [ + displayValue(credential.sourceProfile), + displayValue(credential.effectiveBackendProfile), displayValue(credential.namespace), displayValue(credential.name), Array.isArray(credential.keys) ? credential.keys.map(String).join(",") : "-", diff --git a/scripts/src/agentrun/render.ts b/scripts/src/agentrun/render.ts index 10a34ed4..8c3013fb 100644 --- a/scripts/src/agentrun/render.ts +++ b/scripts/src/agentrun/render.ts @@ -38,7 +38,7 @@ import { agentRunDryRunPlan, readAgentRunClientConfig } from "./config"; import { status } from "./control-plane"; import { arrayRecords, displayValue, isRecord, nextPagedResourceCommand, parseTaskManifest, pickCompact, relativeAge, renderFailureLines, renderResourceNextLines, renderTable, resourceName, shortId, stringOrDash, truncateMultiline, truncateOneLine } from "./options"; import { activeAgentRunRestTarget, agentRunRestRequest, runAgentRunRestCommand } from "./rest-bridge"; -import { renderSessionSendResult } from "./session-send-render"; +import { renderAipodBindingHumanLines, renderSessionSendResult } from "./session-send-render"; import { record, stringOrNull } from "./utils"; export function resolveAgentRunCancelPolicyTarget(config: UniDeskConfig | null, options: AgentRunResourceOptions): { configPath: string; spec: AgentRunLaneSpec; source: "selected-lane" | "default-lane" } | null { @@ -495,6 +495,7 @@ export function renderMutationSummary(command: string, raw: Record line.length > 0).slice(0, 5); diff --git a/scripts/src/agentrun/rest-bridge.ts b/scripts/src/agentrun/rest-bridge.ts index 8a7b2129..1c141ddc 100644 --- a/scripts/src/agentrun/rest-bridge.ts +++ b/scripts/src/agentrun/rest-bridge.ts @@ -43,6 +43,7 @@ import { arrayRecords, displayValue, isRecord, pickCompact, renderTable, truncat import { innerData, pathValue, renderMachine, renderedCliResult } from "./render"; import { parseResourceOptions, stripAgentRunResourceWrapperArgs } from "./resource-actions"; import { AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS, agentRunGitMirrorRetryDelayMs, agentRunGitMirrorRetrySummary, agentRunGitMirrorRetryableFailure, createYamlLaneJobScript, yamlLaneGitMirrorJobManifest, yamlLaneGitMirrorStatusScript, yamlLaneJobProbeScript } from "./secrets"; +import { renderAipodBindingHumanLines } from "./session-send-render"; import { capture, captureJsonPayload, compactCapture, nonNegativeIntegerOrNull, progressEvent, record, shQuote, sleep, stringOrNull } from "./utils"; export async function readGitMirrorStatus(config: UniDeskConfig, target: { configPath: string; spec: AgentRunLaneSpec }): Promise & { result: SshCaptureResult; raw: string; summary: Record }> { @@ -285,10 +286,10 @@ export async function runAgentRunRestCompatCommand(config: UniDeskConfig | null, const options = parseAgentRunRestCompatOptions(args); const command = `agentrun ${canonicalArgs.join(" ")}`.trim(); try { - const raw = await withAgentRunRestTarget(resolveAgentRunRestTarget(config, options), async () => { - return await runAgentRunRestCommand(config, group, stripAgentRunResourceWrapperArgs(args)); + return await withAgentRunRestTarget(resolveAgentRunRestTarget(config, options), async () => { + const raw = await runAgentRunRestCommand(config, group, stripAgentRunResourceWrapperArgs(args)); + return renderAgentRunRestCompatResult(command, group, stripAgentRunResourceWrapperArgs(args), raw, options); }); - return renderAgentRunRestCompatResult(command, group, stripAgentRunResourceWrapperArgs(args), raw, options); } catch (error) { if (error instanceof AgentRunRestError) { if (options.raw || options.full || options.output === "json" || options.output === "yaml") { @@ -335,6 +336,7 @@ export function renderAgentRunAipodSpecRenderSummary(command: string, raw: Recor const namespace = stringOrNull(bridge.namespace) ?? stringOrNull(runnerJobDefaults.namespace) ?? "-"; const providerCredentials = agentRunRenderedProviderCredentials(secretScope, aipod); const toolCredentials = agentRunRenderedToolCredentials(secretScope, aipod); + const aipodBinding = agentRunAipodBindingDisclosure(task, requestedAipod, record(data.modelResolution)); const bundleNames = agentRunResourceBundleNames(resourceBundle.bundles, "name"); const skillNames = agentRunResourceBundleNames(resourceBundle.requiredSkills, "name"); const targetArgs = agentRunCompatTargetCliArgs(node, lane); @@ -343,6 +345,7 @@ export function renderAgentRunAipodSpecRenderSummary(command: string, raw: Recor ` AipodSpec: ${displayValue(aipod.name ?? requestedAipod)} hash=${displayValue(aipod.specHash)}`, ` Target: node=${displayValue(node)} lane=${displayValue(lane)} namespace=${displayValue(namespace)} bridge=${displayValue(bridge.mode)}`, ` TaskPolicy: backendProfile=${displayValue(task.backendProfile)} providerId=${displayValue(task.providerId)} queue=${displayValue(task.queue)} lane=${displayValue(task.lane)}`, + ...renderAipodBindingHumanLines(aipodBinding), ` WorkspaceRef: ${agentRunCompactJson(task.workspaceRef, 140)}`, ` Execution: ${agentRunCompactJson(pickCompact(executionPolicy, ["sandbox", "approval", "timeoutMs", "network"]), 160)}`, " Values: secret payloads are not printed; valuesPrinted=false", @@ -565,7 +568,16 @@ export async function runAgentRunRunnerRest(action: string | undefined, id: stri export async function runAgentRunAipodSpecsRest(action: string | undefined, id: string | undefined, args: string[]): Promise> { if (action === "list") return await agentRunRestRequest("agentrun aipod-specs list", "GET", "/api/v1/aipod-specs"); if (action === "show" && id) return await agentRunRestRequest("agentrun aipod-specs show", "GET", `/api/v1/aipod-specs/${encodeURIComponent(id)}`); - if (action === "render" && id) return await agentRunRestRequest("agentrun aipod-specs render", "POST", `/api/v1/aipod-specs/${encodeURIComponent(id)}/render`, await aipodRenderInputFromArgs(args, 2)); + if (action === "render" && id) { + const renderInput = await aipodRenderInputFromArgs(args, 2); + const rendered = await agentRunRestRequest("agentrun aipod-specs render", "POST", `/api/v1/aipod-specs/${encodeURIComponent(id)}/render`, renderInput); + const renderedData = record(innerData(rendered)); + const task = record(renderedData.queueTask); + if (Object.keys(task).length === 0) throw new AgentRunRestError("schema-mismatch", `aipod-spec ${id} render did not return queueTask`); + normalizeAipodRenderedQueueTask(task, args, id); + agentRunAipodBindingDisclosure(task, id, record(renderedData.modelResolution), renderInput); + return rendered; + } if (action === "apply" || action === "set") { const yaml = readYamlInputFromArgs(args); const pathValue = id ? `/api/v1/aipod-specs/${encodeURIComponent(id)}` : "/api/v1/aipod-specs"; @@ -581,13 +593,16 @@ export async function runAgentRunAipodSpecsRest(action: string | undefined, id: export async function submitQueueTaskRest(args: string[]): Promise> { const aipod = agentRunOption(args, "aipod") ?? agentRunOption(args, "aipod-spec"); if (aipod) { - const rendered = await agentRunRestRequest("agentrun aipod-specs render", "POST", `/api/v1/aipod-specs/${encodeURIComponent(aipod)}/render`, await aipodRenderInputFromArgs(args, 2)); - const body = normalizeAipodRenderedQueueTask(record(record(innerData(rendered)).queueTask), args, aipod); + const renderInput = await aipodRenderInputFromArgs(args, 2); + const rendered = await agentRunRestRequest("agentrun aipod-specs render", "POST", `/api/v1/aipod-specs/${encodeURIComponent(aipod)}/render`, renderInput); + const renderedData = record(innerData(rendered)); + const body = normalizeAipodRenderedQueueTask(record(renderedData.queueTask), args, aipod); if (Object.keys(body).length === 0) throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} render did not return queueTask`); + const aipodBinding = agentRunAipodBindingDisclosure(body, aipod, record(renderedData.modelResolution), renderInput); if (agentRunHasFlag(args, "dry-run")) { return agentRunDryRunPlan("queue-submit", "/api/v1/queue/tasks", body, queueSubmitConfirmCommand(args, aipod), "POST", { jsonInput: { source: "aipod-spec", aipod, valuesPrinted: false }, - aipodBinding: agentRunAipodBindingDisclosure(body, aipod), + aipodBinding, }); } return await agentRunRestRequest("agentrun queue submit", "POST", "/api/v1/queue/tasks", body); @@ -706,34 +721,80 @@ export function normalizeAipodRenderedQueueTask(task: Record, a const sessionPolicy = readAgentRunClientConfig().client.sessionPolicy; const policyTarget = resolveAgentRunSessionPolicyTarget(); const explicitProfile = agentRunOption(args, "profile") ?? agentRunOption(args, "backend-profile"); - const renderedProfile = stringOrNull(task.backendProfile); - const fallbackProfile = sessionPolicy.backendProfile; - let backendProfile = explicitProfile ?? renderedProfile ?? fallbackProfile; - if (agentRunProviderCredentialRefs(policyTarget.spec, backendProfile).length === 0) { - if (explicitProfile !== null) { - throw new AgentRunRestError("validation-failed", `${policyTarget.configPath} has no providerCredential Secret binding for explicit --backend-profile ${explicitProfile} on ${policyTarget.spec.nodeId}/${policyTarget.spec.lane}`); - } - backendProfile = fallbackProfile; + if (explicitProfile !== null) { + throw new AgentRunRestError("validation-failed", `AipodSpec ${aipod} owns backendProfile; explicit profile override ${explicitProfile} is not allowed`); } - if (agentRunProviderCredentialRefs(policyTarget.spec, backendProfile).length === 0) { - throw new AgentRunRestError("validation-failed", `${policyTarget.configPath} has no providerCredential Secret binding for AipodSpec ${aipod} backendProfile=${backendProfile} on ${policyTarget.spec.nodeId}/${policyTarget.spec.lane}`); + const renderedProfile = stringOrNull(task.backendProfile); + if (renderedProfile === null) { + throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} render did not return queueTask.backendProfile`); + } + const providerCredentials = agentRunProviderCredentialRefs(policyTarget.spec, renderedProfile); + if (providerCredentials.length === 0) { + throw new AgentRunRestError("validation-failed", `${policyTarget.configPath} has no providerCredential Secret binding for AipodSpec ${aipod} backendProfile=${renderedProfile} on ${policyTarget.spec.nodeId}/${policyTarget.spec.lane}`); } const basePolicy = record(task.executionPolicy); + if (Object.keys(basePolicy).length === 0) { + throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} render did not return queueTask.executionPolicy`); + } + validateAipodRenderedProviderCredentials(basePolicy, renderedProfile, providerCredentials, policyTarget, aipod); const normalized: Record = { ...task }; - const explicitProviderId = agentRunOption(args, "provider-id"); normalized.tenantId = stringOrNull(normalized.tenantId) ?? sessionPolicy.tenantId; normalized.projectId = stringOrNull(normalized.projectId) ?? sessionPolicy.projectId; - normalized.providerId = explicitProviderId ?? defaultAgentRunProviderId(sessionPolicy, policyTarget); - normalized.backendProfile = backendProfile; + normalized.providerId = stringOrNull(normalized.providerId) ?? defaultAgentRunProviderId(sessionPolicy, policyTarget); + normalized.backendProfile = renderedProfile; if (!isRecord(normalized.workspaceRef)) normalized.workspaceRef = cloneJsonRecord(sessionPolicy.workspaceRef); - normalized.executionPolicy = defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy, policyTarget, { + normalized.executionPolicy = defaultAgentRunExecutionPolicy(renderedProfile, sessionPolicy, policyTarget, { includeToolCredentials: true, - basePolicy: Object.keys(basePolicy).length === 0 ? sessionPolicy.executionPolicy : basePolicy, + basePolicy, }); return normalized; } -export function agentRunAipodBindingDisclosure(task: Record, aipod: string): Record { +function validateAipodRenderedProviderCredentials( + executionPolicy: Record, + backendProfile: string, + expectedCredentials: readonly ReturnType[number][], + policyTarget: ReturnType, + aipod: string, +): void { + const rendered = arrayRecords(record(executionPolicy.secretScope).providerCredentials); + if (rendered.length === 0) { + throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} render did not return executionPolicy.secretScope.providerCredentials`); + } + const expectedByName = new Map(expectedCredentials.map((credential) => [credential.secretRef.name, credential])); + const seen = new Set(); + for (const [index, credential] of rendered.entries()) { + const renderedProfile = stringOrNull(credential.profile); + if (renderedProfile !== backendProfile) { + throw new AgentRunRestError("validation-failed", `aipod-spec ${aipod} providerCredentials[${index}].profile=${renderedProfile ?? "(missing)"} must exactly match queueTask.backendProfile=${backendProfile}`); + } + const secretRef = record(credential.secretRef); + const name = stringOrNull(secretRef.name); + const expected = name === null ? undefined : expectedByName.get(name); + if (name === null || expected === undefined || seen.has(name)) { + throw new AgentRunRestError("validation-failed", `aipod-spec ${aipod} provider credential SecretRef ${name ?? "(missing)"} is not the unique YAML binding for backendProfile=${backendProfile}`); + } + seen.add(name); + const namespace = stringOrNull(secretRef.namespace); + if (namespace !== null && namespace !== expected.secretRef.namespace) { + throw new AgentRunRestError("validation-failed", `aipod-spec ${aipod} provider credential ${name} namespace conflicts with YAML lane ${policyTarget.spec.nodeId}/${policyTarget.spec.lane}`); + } + const keys = Array.isArray(secretRef.keys) ? secretRef.keys.map(String) : []; + if (JSON.stringify([...keys].sort()) !== JSON.stringify([...expected.secretRef.keys].sort())) { + throw new AgentRunRestError("validation-failed", `aipod-spec ${aipod} provider credential ${name} keys conflict with YAML lane ${policyTarget.spec.nodeId}/${policyTarget.spec.lane}`); + } + } + if (seen.size !== expectedCredentials.length) { + throw new AgentRunRestError("validation-failed", `aipod-spec ${aipod} provider credential bindings are incomplete for backendProfile=${backendProfile}`); + } +} + +export function agentRunAipodBindingDisclosure( + task: Record, + aipod: string, + modelResolution: Record, + renderInput: Record | null = null, +): Record { const policyTarget = resolveAgentRunSessionPolicyTarget(); const executionPolicy = record(task.executionPolicy); const secretScope = record(executionPolicy.secretScope); @@ -755,6 +816,42 @@ export function agentRunAipodBindingDisclosure(task: Record, ai projection: record(credential.projection), valuesPrinted: false, })); + const payload = record(task.payload); + const modelConfig = record(payload.modelConfig); + const model = requiredAipodResolutionString(modelResolution.model, aipod, "modelResolution.model"); + const reasoningEffort = requiredAipodResolutionString(modelResolution.reasoningEffort, aipod, "modelResolution.reasoningEffort"); + const modelSource = requiredAipodResolutionSource(modelResolution.modelSource, aipod, "modelResolution.modelSource"); + const reasoningEffortSource = requiredAipodResolutionSource(modelResolution.reasoningEffortSource, aipod, "modelResolution.reasoningEffortSource"); + const modelOverridden = requiredAipodResolutionBoolean(modelResolution.modelOverridden, aipod, "modelResolution.modelOverridden"); + const reasoningEffortOverridden = requiredAipodResolutionBoolean(modelResolution.reasoningEffortOverridden, aipod, "modelResolution.reasoningEffortOverridden"); + const backendProfile = requiredAipodResolutionString(task.backendProfile, aipod, "queueTask.backendProfile"); + const resolvedBackendProfile = requiredAipodResolutionString(modelResolution.backendProfile, aipod, "modelResolution.backendProfile"); + const backendProfileSource = requiredAipodResolutionString(modelResolution.backendProfileSource, aipod, "modelResolution.backendProfileSource"); + assertAipodResolutionEqual(payload.model, model, aipod, "queueTask.payload.model", "modelResolution.model"); + assertAipodResolutionEqual(payload.reasoningEffort, reasoningEffort, aipod, "queueTask.payload.reasoningEffort", "modelResolution.reasoningEffort"); + assertAipodResolutionEqual(modelConfig.model, model, aipod, "queueTask.payload.modelConfig.model", "modelResolution.model"); + assertAipodResolutionEqual(modelConfig.reasoningEffort, reasoningEffort, aipod, "queueTask.payload.modelConfig.reasoningEffort", "modelResolution.reasoningEffort"); + assertAipodResolutionEqual(modelConfig.modelSource, modelSource, aipod, "queueTask.payload.modelConfig.modelSource", "modelResolution.modelSource"); + assertAipodResolutionEqual(modelConfig.reasoningEffortSource, reasoningEffortSource, aipod, "queueTask.payload.modelConfig.reasoningEffortSource", "modelResolution.reasoningEffortSource"); + if (modelConfig.valuesPrinted !== false || modelResolution.valuesPrinted !== false) { + throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} model resolution must disclose valuesPrinted=false`); + } + if (modelOverridden !== (modelSource === "render-input-override") || reasoningEffortOverridden !== (reasoningEffortSource === "render-input-override")) { + throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} model resolution override booleans conflict with source fields`); + } + if (renderInput !== null) { + validateAipodRenderOverride(renderInput, "model", model, modelSource, modelOverridden, aipod); + validateAipodRenderOverride(renderInput, "reasoningEffort", reasoningEffort, reasoningEffortSource, reasoningEffortOverridden, aipod); + } + if (resolvedBackendProfile !== backendProfile || backendProfileSource !== "aipod-spec") { + throw new AgentRunRestError("validation-failed", `aipod-spec ${aipod} modelResolution backendProfile must match queueTask.backendProfile and remain aipod-spec-owned`); + } + const credentialBindings = agentRunProviderCredentialRefs(policyTarget.spec, backendProfile); + if (credentialBindings.length !== 1) { + throw new AgentRunRestError("validation-failed", `${policyTarget.configPath} must declare one unique providerCredential Secret binding for AipodSpec ${aipod} backendProfile=${backendProfile}`); + } + const credentialBinding = credentialBindings[0]!; + validateAipodModelResolutionCredential(modelResolution, credentialBinding, aipod); return { aipod, node: policyTarget.spec.nodeId, @@ -762,7 +859,15 @@ export function agentRunAipodBindingDisclosure(task: Record, ai namespace: policyTarget.spec.runtime.namespace, policySource: policyTarget.source, providerId: task.providerId ?? null, - backendProfile: task.backendProfile ?? null, + sourceProfile: credentialBinding.sourceProfile, + effectiveBackendProfile: backendProfile, + backendProfileSource, + model, + reasoningEffort, + modelSource, + reasoningEffortSource, + modelOverridden, + reasoningEffortOverridden, workspaceRef: task.workspaceRef ?? null, executionPolicy: pickCompact(executionPolicy, ["sandbox", "approval", "timeoutMs", "network"]), providerCredentials, @@ -771,6 +876,70 @@ export function agentRunAipodBindingDisclosure(task: Record, ai }; } +function requiredAipodResolutionString(value: unknown, aipod: string, pathValue: string): string { + const parsed = stringOrNull(value); + if (parsed === null) throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} render did not return non-empty ${pathValue}`); + return parsed; +} + +function requiredAipodResolutionSource(value: unknown, aipod: string, pathValue: string): "aipod-spec-default" | "render-input-override" { + const parsed = requiredAipodResolutionString(value, aipod, pathValue); + if (parsed !== "aipod-spec-default" && parsed !== "render-input-override") { + throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} ${pathValue} must be aipod-spec-default or render-input-override`); + } + return parsed; +} + +function requiredAipodResolutionBoolean(value: unknown, aipod: string, pathValue: string): boolean { + if (typeof value !== "boolean") throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} render did not return boolean ${pathValue}`); + return value; +} + +function assertAipodResolutionEqual(actual: unknown, expected: string, aipod: string, actualPath: string, expectedPath: string): void { + if (actual !== expected) throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} ${actualPath} must match ${expectedPath}`); +} + +function validateAipodRenderOverride( + renderInput: Record, + key: "model" | "reasoningEffort", + effectiveValue: string, + source: "aipod-spec-default" | "render-input-override", + overridden: boolean, + aipod: string, +): void { + const requested = Object.hasOwn(renderInput, key); + const expectedSource = requested ? "render-input-override" : "aipod-spec-default"; + if (source !== expectedSource || overridden !== requested) { + throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} ${key} override disclosure does not match render input`); + } + if (requested && renderInput[key] !== effectiveValue) { + throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} effective ${key} does not match the explicit render input`); + } +} + +function validateAipodModelResolutionCredential( + modelResolution: Record, + expected: ReturnType[number], + aipod: string, +): void { + const credential = record(modelResolution.providerCredential); + const secretRef = record(credential.secretRef); + const keys = Array.isArray(secretRef.keys) && secretRef.keys.every((key) => typeof key === "string") + ? secretRef.keys.map(String).sort() + : null; + if ( + credential.profile !== expected.profile + || secretRef.namespace !== expected.secretRef.namespace + || secretRef.name !== expected.secretRef.name + || keys === null + || JSON.stringify(keys) !== JSON.stringify([...expected.secretRef.keys].sort()) + || credential.valuesPrinted !== false + || secretRef.valuesPrinted !== false + ) { + throw new AgentRunRestError("validation-failed", `aipod-spec ${aipod} modelResolution.providerCredential must match the unique YAML SecretRef for backendProfile=${expected.profile}`); + } +} + export function agentRunSessionRunPolicyDisclosure(runBody: Record): Record { const policyTarget = resolveAgentRunSessionPolicyTarget(); const executionPolicy = record(runBody.executionPolicy); @@ -798,17 +967,20 @@ export function agentRunSessionRunPolicyDisclosure(runBody: Record> { - const rendered = await agentRunRestRequest("agentrun aipod-specs render", "POST", `/api/v1/aipod-specs/${encodeURIComponent(aipod)}/render`, await aipodRenderInputFromArgs(args, 2, { sessionId })); + const renderInput = await aipodRenderInputFromArgs(args, 2, { sessionId }); + const rendered = await agentRunRestRequest("agentrun aipod-specs render", "POST", `/api/v1/aipod-specs/${encodeURIComponent(aipod)}/render`, renderInput); const renderedData = record(innerData(rendered)); const task = normalizeAipodRenderedQueueTask(record(renderedData.queueTask), args, aipod); if (Object.keys(task).length === 0) throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} render did not return queueTask`); + const aipodBinding = agentRunAipodBindingDisclosure(task, aipod, record(renderedData.modelResolution), renderInput); const sessionRef = record(task.sessionRef); const metadata = record(sessionRef.metadata); const title = agentRunOption(args, "title") ?? stringOrNull(task.title); if (title) metadata.title = title; const sessionPolicy = readAgentRunClientConfig().client.sessionPolicy; const policyTarget = resolveAgentRunSessionPolicyTarget(); - const backendProfile = stringOrNull(task.backendProfile) ?? sessionPolicy.backendProfile; + const backendProfile = stringOrNull(task.backendProfile); + if (backendProfile === null) throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} normalized queueTask has no backendProfile`); const executionPolicy = record(task.executionPolicy); const runBody: Record = { tenantId: stringOrNull(task.tenantId) ?? sessionPolicy.tenantId, @@ -843,7 +1015,7 @@ export async function sessionSendWithAipodRest(sessionId: string, aipod: string, aipod, profile: String(task.backendProfile ?? ""), sessionPolicy: agentRunSessionRunPolicyDisclosure(runBody), - aipodBinding: agentRunAipodBindingDisclosure(task, aipod), + aipodBinding, valuesPrinted: false, }, bridge: response.bridge, diff --git a/scripts/src/agentrun/secret-sync-output.ts b/scripts/src/agentrun/secret-sync-output.ts index 53fd76ea..9c2add50 100644 --- a/scripts/src/agentrun/secret-sync-output.ts +++ b/scripts/src/agentrun/secret-sync-output.ts @@ -116,14 +116,17 @@ export function renderAgentRunSecretSyncText(payload: Record): `SECRET PLAN count=${displayValue(plan.secretCount)} omitted=${displayValue(plan.omittedCount)}`, planItems.length === 0 ? "-" - : renderTable(["ID", "TARGET", "KEY", "SOURCE", "PRESENT", "FINGERPRINT", "STATUS"], planItems.map((item) => { + : renderTable(["ID", "TARGET", "KEY", "SOURCE", "SOURCE PROFILE", "BACKEND PROFILE", "PRESENT", "FINGERPRINT", "STATUS"], planItems.map((item) => { const source = record(item.source); const target = record(item.target); + const profile = record(item.profile); return [ displayValue(item.id), displayValue(target.identity), displayValue(target.key), displayValue(source.mode), + displayValue(profile.sourceProfile), + displayValue(profile.effectiveBackendProfile), displayValue(source.present), shortFingerprint(source.fingerprint), source.present === true ? "ready" : "unavailable", @@ -201,6 +204,10 @@ function compactSecretPlanItem(item: Record, identity: Record; runnerAdmission: Record | null; request: Record | null; + aipodBinding: Record | null; error: Record | null; next: { actions: Array<{ command: string; mutation: boolean; reason: string }>; @@ -51,6 +52,7 @@ export function sessionSendSuccessProjection(command: string, raw: Record): Sessio }), runnerAdmission, request: null, + aipodBinding: null, error: errorProjection, next: { actions: nextActions, @@ -237,6 +241,7 @@ function renderSessionSendHuman(command: string, projection: SessionSendProjecti lines.push(`CreateRunner: ${display(projection.createRunner)}`); lines.push(`Mutation: ${display(projection.mutation)}`); lines.push(`PartialWrite: ${display(projection.partialWrite)}`); + if (projection.aipodBinding !== null) lines.push(...renderAipodBindingHumanLines(projection.aipodBinding)); const resources = Object.values(projection.resources).filter((value) => value !== null).map(record); if (resources.length > 0) { lines.push("", "Resources:"); @@ -270,6 +275,59 @@ function renderSessionSendHuman(command: string, projection: SessionSendProjecti return { ok: projection.ok, command, renderedText: lines.join("\n"), contentType: "text/plain" }; } +export function compactAipodBinding(value: Record): Record | null { + if (Object.keys(value).length === 0) return null; + const providerCredentials = Array.isArray(value.providerCredentials) + ? value.providerCredentials.slice(0, 4).map(record).map((credential) => { + const secretRef = record(credential.secretRef); + return { + profile: stringOrNull(credential.profile), + secretRef: { + name: stringOrNull(secretRef.name), + keys: Array.isArray(secretRef.keys) ? secretRef.keys.filter((key): key is string => typeof key === "string").slice(0, 8) : [], + }, + valuesPrinted: false, + }; + }) + : []; + return { + aipod: stringOrNull(value.aipod), + node: stringOrNull(value.node), + lane: stringOrNull(value.lane), + namespace: stringOrNull(value.namespace), + sourceProfile: stringOrNull(value.sourceProfile), + effectiveBackendProfile: stringOrNull(value.effectiveBackendProfile), + backendProfileSource: stringOrNull(value.backendProfileSource), + model: stringOrNull(value.model), + reasoningEffort: stringOrNull(value.reasoningEffort), + modelSource: stringOrNull(value.modelSource), + reasoningEffortSource: stringOrNull(value.reasoningEffortSource), + modelOverridden: typeof value.modelOverridden === "boolean" ? value.modelOverridden : null, + reasoningEffortOverridden: typeof value.reasoningEffortOverridden === "boolean" ? value.reasoningEffortOverridden : null, + providerCredentials, + valuesPrinted: false, + }; +} + +export function renderAipodBindingHumanLines(value: Record): string[] { + const binding = compactAipodBinding(value); + if (binding === null) return []; + const credentials = Array.isArray(binding.providerCredentials) ? binding.providerCredentials.map(record) : []; + const lines = [ + "", + "AIPOD BINDING", + ` Provider: sourceProfile=${display(binding.sourceProfile)} effectiveBackendProfile=${display(binding.effectiveBackendProfile)} source=${display(binding.backendProfileSource)}`, + ` Model: model=${display(binding.model)} reasoningEffort=${display(binding.reasoningEffort)}`, + ` Resolution: modelSource=${display(binding.modelSource)} modelOverridden=${display(binding.modelOverridden)} reasoningEffortSource=${display(binding.reasoningEffortSource)} reasoningEffortOverridden=${display(binding.reasoningEffortOverridden)}`, + ]; + for (const credential of credentials) { + const secretRef = record(credential.secretRef); + const keys = Array.isArray(secretRef.keys) ? secretRef.keys.map(String).join(",") : ""; + lines.push(` SecretRef: profile=${display(credential.profile)} ref=${display(binding.namespace)}/${display(secretRef.name)} keys=${keys || "-"} valuesPrinted=false`); + } + return lines; +} + function compactResources(resources: Record | null>): Record { const result: Record = { session: resources.session, diff --git a/scripts/src/agentrun/trigger.ts b/scripts/src/agentrun/trigger.ts index 06d1ae31..10eee341 100644 --- a/scripts/src/agentrun/trigger.ts +++ b/scripts/src/agentrun/trigger.ts @@ -149,13 +149,15 @@ export async function restartYamlLane(config: UniDeskConfig, options: LaneConfir namespace: secret.targetRef.namespace, name: secret.targetRef.name, key: secret.targetRef.key, - providerCredentialProfile: secret.providerCredentialProfile, + sourceProfile: secret.providerCredentialSourceProfile, + effectiveBackendProfile: secret.providerCredentialProfile, valuesPrinted: false, })), valuesPrinted: false, }, providerCredentials: agentRunProviderCredentialRefs(spec).map((credential) => ({ - profile: credential.profile, + sourceProfile: credential.sourceProfile, + effectiveBackendProfile: credential.profile, namespace: credential.secretRef.namespace, name: credential.secretRef.name, keys: credential.secretRef.keys, @@ -228,6 +230,8 @@ export async function secretSync(config: UniDeskConfig, options: SecretSyncOptio sourceMode: item.sourceMode, sourceKey: item.sourceKey, transform: item.transform ?? null, + sourceProfile: item.providerCredentialSourceProfile ?? null, + effectiveBackendProfile: item.providerCredentialProfile ?? null, ...inspection.summary, valuesPrinted: false, }));