@oisincoveney/pipeline 3.12.4 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,6 +20,11 @@ declare const submitRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
20
20
  kubeconfigPath: z.ZodOptional<z.ZodString>;
21
21
  name: z.ZodOptional<z.ZodString>;
22
22
  namespace: z.ZodString;
23
+ brokerAuth: z.ZodOptional<z.ZodObject<{
24
+ secretKey: z.ZodDefault<z.ZodString>;
25
+ secretName: z.ZodString;
26
+ url: z.ZodDefault<z.ZodString>;
27
+ }, z.core.$strict>>;
23
28
  opencodeAuthSecretName: z.ZodOptional<z.ZodString>;
24
29
  opencodeOpenaiAccountsSecretName: z.ZodOptional<z.ZodString>;
25
30
  payloadJson: z.ZodString;
@@ -1,4 +1,5 @@
1
1
  import { ArgoGraphCompilerError, compileArgoExecutionGraph } from "./argo-graph.js";
2
+ import { brokerAuthOptionSchema } from "./broker-auth.js";
2
3
  import { parseRunnerCommandPayload, runnerCommandPayloadSchema } from "./runner-command-contract.js";
3
4
  import { buildRunnerTaskDescriptor } from "./runner-command/task-descriptor.js";
4
5
  import { buildRunnerArgoWorkflowManifest, runnerArgoWorkflowManifestSchema } from "./argo-workflow.js";
@@ -38,6 +39,7 @@ const submitRunnerArgoWorkflowOptionsSchema = z.object({
38
39
  kubeconfigPath: z.string().min(1).optional(),
39
40
  name: z.string().min(1).optional(),
40
41
  namespace: z.string().min(1),
42
+ brokerAuth: brokerAuthOptionSchema.optional(),
41
43
  opencodeAuthSecretName: z.string().min(1).optional(),
42
44
  opencodeOpenaiAccountsSecretName: z.string().min(1).optional(),
43
45
  payloadJson: z.string().min(1),
@@ -89,6 +91,7 @@ function submitRunnerArgoWorkflowEffect(rawOptions, dependencies) {
89
91
  labels,
90
92
  name: options.name,
91
93
  namespace: options.namespace,
94
+ brokerAuth: options.brokerAuth,
92
95
  opencodeAuthSecretName: options.opencodeAuthSecretName,
93
96
  opencodeOpenaiAccountsSecret: options.opencodeOpenaiAccountsSecretName ? { name: options.opencodeOpenaiAccountsSecretName } : void 0,
94
97
  payloadConfigMapName,
@@ -27,10 +27,18 @@ declare const runnerArgoWorkflowManifestSchema: z.ZodObject<{
27
27
  container: z.ZodOptional<z.ZodObject<{
28
28
  args: z.ZodArray<z.ZodString>;
29
29
  command: z.ZodOptional<z.ZodArray<z.ZodString>>;
30
- env: z.ZodOptional<z.ZodArray<z.ZodObject<{
30
+ env: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
31
31
  name: z.ZodString;
32
32
  value: z.ZodString;
33
- }, z.core.$strict>>>;
33
+ }, z.core.$strict>, z.ZodObject<{
34
+ name: z.ZodString;
35
+ valueFrom: z.ZodObject<{
36
+ secretKeyRef: z.ZodObject<{
37
+ key: z.ZodString;
38
+ name: z.ZodString;
39
+ }, z.core.$strict>;
40
+ }, z.core.$strict>;
41
+ }, z.core.$strict>]>>>;
34
42
  image: z.ZodString;
35
43
  imagePullPolicy: z.ZodEnum<{
36
44
  Always: "Always";
@@ -137,6 +145,11 @@ declare const buildRunnerArgoWorkflowOptionsSchema: z.ZodObject<{
137
145
  labels: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodOptional<z.ZodString>>>;
138
146
  name: z.ZodOptional<z.ZodString>;
139
147
  namespace: z.ZodString;
148
+ brokerAuth: z.ZodOptional<z.ZodObject<{
149
+ secretKey: z.ZodDefault<z.ZodString>;
150
+ secretName: z.ZodString;
151
+ url: z.ZodDefault<z.ZodString>;
152
+ }, z.core.$strict>>;
140
153
  opencodeAuthSecretName: z.ZodOptional<z.ZodString>;
141
154
  opencodeOpenaiAccountsSecret: z.ZodOptional<z.ZodObject<{
142
155
  key: z.ZodOptional<z.ZodString>;
@@ -92,14 +92,21 @@ const argoWorkflowRetryStrategySchema = z.object({
92
92
  "OnTransientError"
93
93
  ])
94
94
  }).strict();
95
+ const argoWorkflowEnvVarSchema = z.union([z.object({
96
+ name: z.string().min(1),
97
+ value: z.string()
98
+ }).strict(), z.object({
99
+ name: z.string().min(1),
100
+ valueFrom: z.object({ secretKeyRef: z.object({
101
+ key: z.string().min(1),
102
+ name: kubernetesNameSchema
103
+ }).strict() }).strict()
104
+ }).strict()]);
95
105
  const argoWorkflowTemplateSchema = z.object({
96
106
  container: z.object({
97
107
  args: z.array(z.string().min(1)).min(1),
98
108
  command: z.array(z.string().min(1)).min(1).optional(),
99
- env: z.array(z.object({
100
- name: z.string().min(1),
101
- value: z.string()
102
- }).strict()).optional(),
109
+ env: z.array(argoWorkflowEnvVarSchema).optional(),
103
110
  image: z.string().min(1),
104
111
  imagePullPolicy: z.enum([
105
112
  "Always",
@@ -169,6 +176,11 @@ const buildRunnerArgoWorkflowOptionsSchema = z.object({
169
176
  labels: z.record(z.string().min(1), z.string().min(1).optional()).default({}),
170
177
  name: z.string().min(1).optional(),
171
178
  namespace: kubernetesNameSchema,
179
+ brokerAuth: z.object({
180
+ secretKey: z.string().min(1).default("api-key"),
181
+ secretName: kubernetesNameSchema,
182
+ url: z.string().min(1).default("https://cliproxy.momokaya.ee")
183
+ }).strict().optional(),
172
184
  opencodeAuthSecretName: kubernetesNameSchema.optional(),
173
185
  opencodeOpenaiAccountsSecret: z.object({
174
186
  key: z.string().min(1).optional(),
@@ -374,6 +386,28 @@ function runnerWorkflowStorage(options, tasks) {
374
386
  volumes: z.array(argoWorkflowVolumeSchema).parse(volumes)
375
387
  };
376
388
  }
389
+ /**
390
+ * The runner container env: the static opencode/agent tuning plus, when broker
391
+ * auth is configured, BROKER_URL (literal) and BROKER_API_KEY (sourced from the
392
+ * broker secret key, never inlined into the manifest).
393
+ */
394
+ function runnerContainerEnv(options) {
395
+ if (!options.brokerAuth) return [...RUNNER_OPENCODE_ENV];
396
+ return [
397
+ ...RUNNER_OPENCODE_ENV,
398
+ {
399
+ name: "BROKER_URL",
400
+ value: options.brokerAuth.url
401
+ },
402
+ {
403
+ name: "BROKER_API_KEY",
404
+ valueFrom: { secretKeyRef: {
405
+ key: options.brokerAuth.secretKey,
406
+ name: options.brokerAuth.secretName
407
+ } }
408
+ }
409
+ ];
410
+ }
377
411
  function runnerLifecycleTemplate(options, volumeMounts) {
378
412
  return {
379
413
  container: {
@@ -387,7 +421,7 @@ function runnerLifecycleTemplate(options, volumeMounts) {
387
421
  RUNNER_WORKFLOW_SCHEDULE_PATH
388
422
  ],
389
423
  command: ["moka"],
390
- env: [...RUNNER_OPENCODE_ENV],
424
+ env: runnerContainerEnv(options),
391
425
  image: options.image,
392
426
  imagePullPolicy: options.imagePullPolicy,
393
427
  name: "runner",
@@ -416,7 +450,7 @@ function runnerCommandTemplate(task, options, volumeMounts) {
416
450
  RUNNER_WORKFLOW_SCHEDULE_PATH
417
451
  ],
418
452
  command: ["moka"],
419
- env: [...RUNNER_OPENCODE_ENV],
453
+ env: runnerContainerEnv(options),
420
454
  image: options.image,
421
455
  imagePullPolicy: options.imagePullPolicy,
422
456
  name: "runner",
@@ -441,7 +475,7 @@ function runnerFinalizerTemplate(options, volumeMounts) {
441
475
  "{{workflow.status}}"
442
476
  ],
443
477
  command: ["moka"],
444
- env: [...RUNNER_OPENCODE_ENV],
478
+ env: runnerContainerEnv(options),
445
479
  image: options.image,
446
480
  imagePullPolicy: options.imagePullPolicy,
447
481
  name: "runner",
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/broker-auth.d.ts
4
+ /**
5
+ * Submit-time broker auth options: the runner sources BROKER_API_KEY from
6
+ * `secretName[secretKey]` and BROKER_URL from `url`. Shared by every submit
7
+ * entrypoint so the broker wiring is declared in exactly one place.
8
+ */
9
+ declare const brokerAuthOptionSchema: z.ZodObject<{
10
+ secretKey: z.ZodDefault<z.ZodString>;
11
+ secretName: z.ZodString;
12
+ url: z.ZodDefault<z.ZodString>;
13
+ }, z.core.$strict>;
14
+ type BrokerAuthOption = z.input<typeof brokerAuthOptionSchema>;
15
+ //#endregion
16
+ export { BrokerAuthOption };
@@ -0,0 +1,172 @@
1
+ import { applyJsonEdit, ensureTrailingNewline, formatJson, parseJsonRecord } from "./json-config-merge.js";
2
+ import { z } from "zod";
3
+ //#region src/broker-auth.ts
4
+ /**
5
+ * Central CLIProxyAPI broker auth for codex + opencode.
6
+ *
7
+ * When `BROKER_API_KEY` is present, codex and opencode authenticate through the
8
+ * central broker (an OpenAI-compatible `/v1` endpoint) instead of materializing
9
+ * the bespoke multi-auth account pool. The broker owns OAuth refresh / rotation
10
+ * / failover, so the runner no longer stages `oc-codex-multi-auth` accounts, the
11
+ * mounted `~/.codex/auth.json`, or the multi-auth opencode plugin.
12
+ *
13
+ * This mirrors the proven coder dev-workspace template
14
+ * (infra: coder-templates/dev-workspace/main.tf):
15
+ * - codex ~/.codex/config.toml: `model_provider = "broker"` +
16
+ * `[model_providers.broker]` base_url=<broker>/v1 env_key=BROKER_API_KEY
17
+ * wire_api="responses".
18
+ * - opencode global config: `provider.openai.options.baseURL=<broker>/v1`
19
+ * plus `store=false` and `include=["reasoning.encrypted_content"]` (required
20
+ * by the Codex/Responses backend the broker fronts).
21
+ * - opencode auth store: `{"openai":{"type":"api","key":<BROKER_API_KEY>}}`.
22
+ */
23
+ const BROKER_API_KEY_ENV = "BROKER_API_KEY";
24
+ const BROKER_URL_ENV = "BROKER_URL";
25
+ const DEFAULT_BROKER_URL = "https://cliproxy.momokaya.ee";
26
+ const TRAILING_SLASH_RE = /\/+$/;
27
+ const CODEX_BROKER_PROVIDER_ID = "broker";
28
+ const OPENCODE_OPENAI_PROVIDER_ID = "openai";
29
+ /** Plugin removed from opencode config in broker mode (broker fronts auth). */
30
+ const OC_CODEX_MULTI_AUTH_PLUGIN_NAME = "oc-codex-multi-auth";
31
+ /**
32
+ * Submit-time broker auth options: the runner sources BROKER_API_KEY from
33
+ * `secretName[secretKey]` and BROKER_URL from `url`. Shared by every submit
34
+ * entrypoint so the broker wiring is declared in exactly one place.
35
+ */
36
+ const brokerAuthOptionSchema = z.object({
37
+ secretKey: z.string().min(1).default("api-key"),
38
+ secretName: z.string().min(1),
39
+ url: z.string().min(1).default(DEFAULT_BROKER_URL)
40
+ }).strict();
41
+ /**
42
+ * Resolve broker credentials from the environment, or `undefined` when the
43
+ * runner is not broker-authenticated (local dev, non-broker fallback). The
44
+ * `BROKER_URL` env is optional and defaults to the production broker origin.
45
+ */
46
+ function resolveBrokerCredentials(env = process.env) {
47
+ const apiKey = env[BROKER_API_KEY_ENV];
48
+ if (apiKey === void 0 || apiKey.length === 0) return;
49
+ const rawUrl = env[BROKER_URL_ENV];
50
+ return {
51
+ apiKey,
52
+ baseUrl: rawUrl !== void 0 && rawUrl.length > 0 ? rawUrl.replace(TRAILING_SLASH_RE, "") : DEFAULT_BROKER_URL
53
+ };
54
+ }
55
+ /** The broker's OpenAI-compatible endpoint (`<baseUrl>/v1`). */
56
+ function brokerV1Url(credentials) {
57
+ return `${credentials.baseUrl}/v1`;
58
+ }
59
+ /**
60
+ * opencode host auth store contents for broker mode — the `openai` provider
61
+ * authenticates with the broker api-key. Other providers in an existing store
62
+ * are intentionally NOT preserved here: in broker mode the runner owns this
63
+ * file outright (the multi-auth pool that previously populated it is gone).
64
+ */
65
+ function renderOpencodeBrokerAuthJson(credentials) {
66
+ return formatJson({ [OPENCODE_OPENAI_PROVIDER_ID]: {
67
+ key: credentials.apiKey,
68
+ type: "api"
69
+ } });
70
+ }
71
+ /**
72
+ * Inject the codex broker model provider into an existing `config.toml`,
73
+ * preserving every other section. Idempotent: re-running replaces the
74
+ * `[model_providers.broker]` block and the top-level `model_provider` key.
75
+ */
76
+ function applyCodexBrokerProvider(currentText, credentials) {
77
+ const withoutProvider = removeCodexBrokerSections(currentText ?? "");
78
+ const projection = renderCodexBrokerProvider(credentials);
79
+ return ensureTrailingNewline([withoutProvider.trimEnd(), projection.trimEnd()].filter(Boolean).join("\n\n"));
80
+ }
81
+ function renderCodexBrokerProvider(credentials) {
82
+ return [
83
+ `model_provider = "${CODEX_BROKER_PROVIDER_ID}"`,
84
+ "",
85
+ `[model_providers.${CODEX_BROKER_PROVIDER_ID}]`,
86
+ `name = "${CODEX_BROKER_PROVIDER_ID}"`,
87
+ `base_url = "${brokerV1Url(credentials)}"`,
88
+ `env_key = "${BROKER_API_KEY_ENV}"`,
89
+ "wire_api = \"responses\""
90
+ ].join("\n");
91
+ }
92
+ const CODEX_BROKER_SECTION_HEADER = `[model_providers.${CODEX_BROKER_PROVIDER_ID}]`;
93
+ const CODEX_MODEL_PROVIDER_KEY_RE = /^\s*model_provider\s*=/;
94
+ function removeCodexBrokerSections(content) {
95
+ const lines = content.split("\n");
96
+ const kept = [];
97
+ let removing = false;
98
+ for (const line of lines) {
99
+ if (line.trim() === CODEX_BROKER_SECTION_HEADER) {
100
+ removing = true;
101
+ continue;
102
+ }
103
+ if (removing && isTomlSectionHeader(line)) removing = false;
104
+ if (removing) continue;
105
+ if (CODEX_MODEL_PROVIDER_KEY_RE.test(line)) continue;
106
+ kept.push(line);
107
+ }
108
+ return kept.join("\n");
109
+ }
110
+ function isTomlSectionHeader(line) {
111
+ const trimmed = line.trim();
112
+ return trimmed.startsWith("[") && trimmed.endsWith("]");
113
+ }
114
+ /**
115
+ * Point an opencode config (global or project `opencode.json`) at the broker:
116
+ * set `provider.openai.options.{baseURL,store,include}` and drop the
117
+ * `oc-codex-multi-auth` plugin entry. Preserves all other config (models, mcp,
118
+ * other plugins). Creates a minimal config when `currentText` is undefined.
119
+ */
120
+ function applyOpencodeBrokerProvider(currentText, credentials) {
121
+ const options = {
122
+ baseURL: brokerV1Url(credentials),
123
+ include: ["reasoning.encrypted_content"],
124
+ store: false
125
+ };
126
+ if (currentText === void 0) return { content: formatJson({
127
+ $schema: "https://opencode.ai/config.json",
128
+ provider: { [OPENCODE_OPENAI_PROVIDER_ID]: { options } }
129
+ }) };
130
+ const parsed = parseJsonRecord(currentText);
131
+ if (!parsed.ok) return { error: "invalid opencode config JSON" };
132
+ const withProvider = applyJsonEdit(currentText, [
133
+ "provider",
134
+ OPENCODE_OPENAI_PROVIDER_ID,
135
+ "options"
136
+ ], mergeOpenaiOptions(parsed.value, options));
137
+ const nextPlugins = pluginsWithoutMultiAuth(parsed.value.plugin);
138
+ return { content: ensureTrailingNewline(nextPlugins === void 0 ? withProvider : applyJsonEdit(withProvider, ["plugin"], nextPlugins)) };
139
+ }
140
+ function mergeOpenaiOptions(parsed, options) {
141
+ return {
142
+ ...currentOpenaiOptions(parsed),
143
+ ...options
144
+ };
145
+ }
146
+ function currentOpenaiOptions(parsed) {
147
+ const provider = parsed.provider;
148
+ if (!isRecord(provider)) return {};
149
+ const openai = provider[OPENCODE_OPENAI_PROVIDER_ID];
150
+ if (!isRecord(openai)) return {};
151
+ return isRecord(openai.options) ? openai.options : {};
152
+ }
153
+ /**
154
+ * Return the plugin array with every `oc-codex-multi-auth` entry removed, or
155
+ * `undefined` when there was nothing to remove (so the caller can skip the
156
+ * edit). Handles bare-string and `[name, opts]` tuple plugin specifiers.
157
+ */
158
+ function pluginsWithoutMultiAuth(plugin) {
159
+ if (!Array.isArray(plugin)) return;
160
+ const filtered = plugin.filter((entry) => !isMultiAuthPlugin(entry));
161
+ return filtered.length === plugin.length ? void 0 : filtered;
162
+ }
163
+ function isMultiAuthPlugin(entry) {
164
+ const specifier = Array.isArray(entry) ? entry[0] : entry;
165
+ if (typeof specifier !== "string") return false;
166
+ return (specifier.indexOf("@", 1) === -1 ? specifier : specifier.slice(0, specifier.indexOf("@", 1))) === OC_CODEX_MULTI_AUTH_PLUGIN_NAME;
167
+ }
168
+ function isRecord(value) {
169
+ return typeof value === "object" && value !== null && !Array.isArray(value);
170
+ }
171
+ //#endregion
172
+ export { applyCodexBrokerProvider, applyOpencodeBrokerProvider, brokerAuthOptionSchema, renderOpencodeBrokerAuthJson, resolveBrokerCredentials };
@@ -415,6 +415,7 @@ function buildLoopSubmitInput(options) {
415
415
  githubAuthSecretName: momokaya?.submit.githubAuthSecretName,
416
416
  kubeconfigPath: momokaya?.kubernetes.kubeconfig,
417
417
  namespace: momokaya?.kubernetes.namespace,
418
+ brokerAuth: momokaya?.submit.brokerAuth,
418
419
  opencodeAuthSecretName: momokaya?.submit.opencodeAuthSecretName,
419
420
  opencodeOpenaiAccountsSecretName: momokaya?.submit.opencodeOpenaiAccountsSecretName,
420
421
  serviceAccountName: momokaya?.submit.serviceAccountName,
@@ -48,6 +48,7 @@ function mokaCommonSubmitOptions(input) {
48
48
  kubeconfigPath: input.flags.kubeconfig ?? momokaya?.kubernetes.kubeconfig,
49
49
  name: input.flags.name,
50
50
  namespace: input.flags.namespace ?? momokaya?.kubernetes.namespace,
51
+ brokerAuth: momokaya?.submit.brokerAuth,
51
52
  opencodeAuthSecretName: momokaya?.submit.opencodeAuthSecretName,
52
53
  opencodeOpenaiAccountsSecretName: momokaya?.submit.opencodeOpenaiAccountsSecretName,
53
54
  serviceAccountName: input.flags.serviceAccount ?? momokaya?.submit.serviceAccountName,
@@ -62,19 +62,21 @@ function clusterResources() {
62
62
  const configured = loadMokaGlobalConfig()?.momokaya.submit;
63
63
  return configured ? {
64
64
  ...DEFAULT_RESOURCES,
65
+ brokerAuthSecretName: configured.brokerAuth?.secretName,
65
66
  eventAuthSecretName: configured.eventAuthSecretName,
66
67
  gitCredentialsSecretName: configured.gitCredentialsSecretName,
67
68
  githubAuthSecretName: configured.githubAuthSecretName,
68
69
  imagePullSecretName: configured.imagePullSecretName,
69
- opencodeAuthSecretName: configured.opencodeAuthSecretName,
70
+ opencodeAuthSecretName: configured.opencodeAuthSecretName ?? DEFAULT_RESOURCES.opencodeAuthSecretName,
70
71
  serviceAccountName: configured.serviceAccountName
71
72
  } : DEFAULT_RESOURCES;
72
73
  }
73
74
  function secretChecks(namespace, kubectlOptions, resources) {
75
+ const authSecretCheck = resources.brokerAuthSecretName ? [resources.brokerAuthSecretName, `Secret ${resources.brokerAuthSecretName} missing in ${namespace}; expected broker api-key mount by name.`] : [resources.opencodeAuthSecretName, `Secret ${resources.opencodeAuthSecretName} missing in ${namespace}; expected OpenCode auth mount by name.`];
74
76
  return [
75
77
  [resources.eventAuthSecretName, eventAuthMissingDetail(namespace)],
76
78
  [resources.imagePullSecretName, `Secret ${resources.imagePullSecretName} missing in ${namespace}; expected imagePullSecret for ghcr.io/oisin-ee/pipeline-runner.`],
77
- [resources.opencodeAuthSecretName, `Secret ${resources.opencodeAuthSecretName} missing in ${namespace}; expected OpenCode auth mount by name.`],
79
+ authSecretCheck,
78
80
  [resources.gitCredentialsSecretName, `Secret ${resources.gitCredentialsSecretName} missing in ${namespace}; expected runner git credentials mount by name.`],
79
81
  [resources.githubAuthSecretName, `Secret ${resources.githubAuthSecretName} missing in ${namespace}; expected GitHub auth mount by name.`]
80
82
  ].map(([name, missingDetail]) => checkNamespacedResource(`secret/${name}`, [
@@ -1,3 +1,4 @@
1
+ import { applyOpencodeBrokerProvider, resolveBrokerCredentials } from "./broker-auth.js";
1
2
  import { mergeOpenCodeProjectConfig } from "./opencode-project-config.js";
2
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
4
  import { homedir } from "node:os";
@@ -7,7 +8,8 @@ import { parse } from "jsonc-parser";
7
8
  const CODEX_MULTI_AUTH_PLUGIN = "oc-codex-multi-auth@6.3.2";
8
9
  const GLOBAL_CODEX_AUTH_CONFIG_PATH = join(homedir(), ".opencode/openai-codex-auth-config.json");
9
10
  function syncLocalCodexAuth(options) {
10
- const items = [syncGlobalPluginConfig(options.globalConfigPath ?? GLOBAL_CODEX_AUTH_CONFIG_PATH, options), ...discoverGitRepositories(options.root).map((repo) => syncProjectOpenCodeConfig(repo, options))];
11
+ const broker = options.broker === void 0 ? resolveBrokerCredentials() : options.broker;
12
+ const items = broker ? discoverGitRepositories(options.root).map((repo) => syncProjectBrokerConfig(repo, broker, options)) : [syncGlobalPluginConfig(options.globalConfigPath ?? GLOBAL_CODEX_AUTH_CONFIG_PATH, options), ...discoverGitRepositories(options.root).map((repo) => syncProjectOpenCodeConfig(repo, options))];
11
13
  const hasRequiredChanges = items.some((item) => item.action === "create" || item.action === "update");
12
14
  const hasErrors = items.some((item) => item.action === "error");
13
15
  const checkFailed = options.check === true && hasRequiredChanges;
@@ -48,6 +50,17 @@ function syncProjectOpenCodeConfig(repo, options) {
48
50
  };
49
51
  return writeIfChanged(path, currentText, merged.content, options);
50
52
  }
53
+ function syncProjectBrokerConfig(repo, broker, options) {
54
+ const path = join(repo, ".opencode/opencode.json");
55
+ const currentText = existsSync(path) ? readFileSync(path, "utf8") : void 0;
56
+ const result = applyOpencodeBrokerProvider(currentText, broker);
57
+ if ("error" in result) return {
58
+ action: "error",
59
+ message: result.error,
60
+ path
61
+ };
62
+ return writeIfChanged(path, currentText, result.content, options);
63
+ }
51
64
  function writeIfChanged(path, currentText, nextText, options) {
52
65
  if (currentText === nextText) return {
53
66
  action: "unchanged",
@@ -1,6 +1,6 @@
1
+ import { CLAUDE_PROJECT_CONFIG_PATH, commandIdForHost, compactLines, entrypointDescription, entrypointEntries, instructionsPointer, invocationForHost } from "./shared.js";
1
2
  import { opencodeAgentName } from "../runtime/opencode-agent-name.js";
2
3
  import { mergeClaudeSettings } from "../claude-settings-config.js";
3
- import { CLAUDE_PROJECT_CONFIG_PATH, commandIdForHost, compactLines, entrypointDescription, entrypointEntries, instructionsPointer, invocationForHost } from "./shared.js";
4
4
  import { agentDispatchRoutes, entrypointDispatchBlock, grants, header, markdown, projectAgentsMdDefinition, resolvedHostModel } from "./opencode.js";
5
5
  //#region src/install-commands/claude-code.ts
6
6
  const CLAUDE_CODE_HOST = "claude-code";
@@ -1,3 +1,4 @@
1
+ import { AGENTS_MD_END, AGENTS_MD_START, COMMAND_HOSTS, GENERATED_MARKER, GENERATED_TS_MARKER, OPENCODE_PROJECT_CONFIG_PATH, OWNER_MARKER_PREFIX, OWNER_TS_MARKER_PREFIX, SINGLE_OPENCODE_PLUGIN_ARRAY_RE, commandIdForHost, compactLines, entrypointDescription, entrypointEntries, instructionsPointer, invocationForHost, profileEntries } from "./shared.js";
1
2
  import { DEFAULT_OPENCODE_ECOSYSTEM_MANIFEST } from "../config/defaults.js";
2
3
  import { resolvePackageAssetPath } from "../package-assets.js";
3
4
  import "../config.js";
@@ -6,7 +7,6 @@ import { renderOpenCodeGatewayConfig } from "../mcp/gateway.js";
6
7
  import { compileWorkflowPlan } from "../planning/compile.js";
7
8
  import { opencodeAgentName } from "../runtime/opencode-agent-name.js";
8
9
  import { mergeOpenCodeProjectConfig } from "../opencode-project-config.js";
9
- import { AGENTS_MD_END, AGENTS_MD_START, COMMAND_HOSTS, GENERATED_MARKER, GENERATED_TS_MARKER, OPENCODE_PROJECT_CONFIG_PATH, OWNER_MARKER_PREFIX, OWNER_TS_MARKER_PREFIX, SINGLE_OPENCODE_PLUGIN_ARRAY_RE, commandIdForHost, compactLines, entrypointDescription, entrypointEntries, instructionsPointer, invocationForHost, profileEntries } from "./shared.js";
10
10
  import { Effect } from "effect";
11
11
  import { basename } from "node:path";
12
12
  import matter from "gray-matter";
@@ -1,10 +1,10 @@
1
+ import { isRecord } from "./json-config-merge.js";
2
+ import { CLAUDE_USER_CONFIG_PATH, CODEX_CONFIG_PATH, COMMAND_HOSTS, ENTRYPOINT_PATH_PATTERNS, INSTALL_HOSTS, invocationForHost, resolveHarnessTarget } from "./install-commands/shared.js";
1
3
  import { loadPipelineConfig } from "./config/load.js";
2
4
  import "./config.js";
3
- import { isRecord } from "./json-config-merge.js";
4
5
  import { mergeClaudeUserConfig } from "./claude-user-config.js";
5
6
  import { mergeCodexConfig } from "./codex-config.js";
6
7
  import { renderClaudeGatewayUserConfig, renderCodexGatewayConfig } from "./mcp/gateway.js";
7
- import { CLAUDE_USER_CONFIG_PATH, CODEX_CONFIG_PATH, COMMAND_HOSTS, ENTRYPOINT_PATH_PATTERNS, INSTALL_HOSTS, invocationForHost, resolveHarnessTarget } from "./install-commands/shared.js";
8
8
  import { opencodeAdapter } from "./install-commands/opencode.js";
9
9
  import { claudeCodeAdapter } from "./install-commands/claude-code.js";
10
10
  import { existsSync, readFileSync, statSync } from "node:fs";
@@ -1,6 +1,6 @@
1
1
  import { applyJsonEdit, ensureTrailingNewline, parseJsonRecord } from "./json-config-merge.js";
2
- import { AGENT_ASSET_SOURCE, AGENT_HOOKS_DIR } from "./agent-assets.js";
3
2
  import { resolveHarnessTarget } from "./install-commands/shared.js";
3
+ import { AGENT_ASSET_SOURCE, AGENT_HOOKS_DIR } from "./agent-assets.js";
4
4
  import { existsSync, readFileSync, statSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
6
  import { dirname, join, relative } from "node:path";
@@ -80,6 +80,7 @@ function loopControllerSubmitInput(input) {
80
80
  image: input.image,
81
81
  kubeconfigPath: input.kubeconfigPath,
82
82
  namespace: input.namespace,
83
+ brokerAuth: input.brokerAuth,
83
84
  opencodeAuthSecretName: input.opencodeAuthSecretName,
84
85
  opencodeOpenaiAccountsSecretName: input.opencodeOpenaiAccountsSecretName,
85
86
  serviceAccountName: input.serviceAccountName,
@@ -9,13 +9,18 @@ declare const mokaGlobalConfigSchema: z.ZodObject<{
9
9
  namespace: z.ZodString;
10
10
  }, z.core.$strict>;
11
11
  submit: z.ZodObject<{
12
+ brokerAuth: z.ZodOptional<z.ZodObject<{
13
+ secretKey: z.ZodDefault<z.ZodString>;
14
+ secretName: z.ZodString;
15
+ url: z.ZodDefault<z.ZodString>;
16
+ }, z.core.$strict>>;
12
17
  eventAuthSecretKey: z.ZodString;
13
18
  eventAuthSecretName: z.ZodString;
14
19
  eventUrl: z.ZodString;
15
20
  gitCredentialsSecretName: z.ZodString;
16
21
  githubAuthSecretName: z.ZodString;
17
22
  imagePullSecretName: z.ZodString;
18
- opencodeAuthSecretName: z.ZodString;
23
+ opencodeAuthSecretName: z.ZodOptional<z.ZodString>;
19
24
  opencodeOpenaiAccountsSecretName: z.ZodOptional<z.ZodString>;
20
25
  serviceAccountName: z.ZodString;
21
26
  }, z.core.$strict>;
@@ -1,3 +1,4 @@
1
+ import { brokerAuthOptionSchema } from "./broker-auth.js";
1
2
  import { PipelineConfigError } from "./config/schemas.js";
2
3
  import { ConfigIoService, runConfigIoSync } from "./runtime/services/config-io-service.js";
3
4
  import "./config.js";
@@ -8,13 +9,14 @@ import { join } from "node:path";
8
9
  //#region src/moka-global-config.ts
9
10
  const MOKA_GLOBAL_CONFIG_PATH = ".config/moka/config.yaml";
10
11
  const mokaSubmitGlobalConfigSchema = z.object({
12
+ brokerAuth: brokerAuthOptionSchema.optional(),
11
13
  eventAuthSecretKey: z.string().min(1),
12
14
  eventAuthSecretName: z.string().min(1),
13
15
  eventUrl: z.string().url(),
14
16
  gitCredentialsSecretName: z.string().min(1),
15
17
  githubAuthSecretName: z.string().min(1),
16
18
  imagePullSecretName: z.string().min(1),
17
- opencodeAuthSecretName: z.string().min(1),
19
+ opencodeAuthSecretName: z.string().min(1).optional(),
18
20
  opencodeOpenaiAccountsSecretName: z.string().min(1).optional(),
19
21
  serviceAccountName: z.string().min(1)
20
22
  }).strict();
@@ -1,4 +1,5 @@
1
1
  import { PipelineConfig } from "./config/schemas.js";
2
+ import { BrokerAuthOption } from "./broker-auth.js";
2
3
  import { generateScheduleArtifact } from "./planning/generate.js";
3
4
  import { z } from "zod";
4
5
 
@@ -151,6 +152,11 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
151
152
  kubeconfigPath: z.ZodOptional<z.ZodString>;
152
153
  name: z.ZodOptional<z.ZodString>;
153
154
  namespace: z.ZodOptional<z.ZodString>;
155
+ brokerAuth: z.ZodOptional<z.ZodObject<{
156
+ secretKey: z.ZodDefault<z.ZodString>;
157
+ secretName: z.ZodString;
158
+ url: z.ZodDefault<z.ZodString>;
159
+ }, z.core.$strict>>;
154
160
  opencodeAuthSecretName: z.ZodOptional<z.ZodString>;
155
161
  opencodeOpenaiAccountsSecretName: z.ZodOptional<z.ZodString>;
156
162
  repository: z.ZodOptional<z.ZodObject<{
@@ -269,6 +275,11 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
269
275
  kubeconfigPath: z.ZodOptional<z.ZodString>;
270
276
  name: z.ZodOptional<z.ZodString>;
271
277
  namespace: z.ZodOptional<z.ZodString>;
278
+ brokerAuth: z.ZodOptional<z.ZodObject<{
279
+ secretKey: z.ZodDefault<z.ZodString>;
280
+ secretName: z.ZodString;
281
+ url: z.ZodDefault<z.ZodString>;
282
+ }, z.core.$strict>>;
272
283
  opencodeAuthSecretName: z.ZodOptional<z.ZodString>;
273
284
  opencodeOpenaiAccountsSecretName: z.ZodOptional<z.ZodString>;
274
285
  repository: z.ZodOptional<z.ZodObject<{
@@ -323,6 +334,7 @@ interface SubmitMokaDependencies {
323
334
  submitWorkflow?: (options: MokaWorkflowSubmitOptions) => Promise<MokaSubmitOutput>;
324
335
  }
325
336
  interface MokaWorkflowSubmitOptions {
337
+ brokerAuth?: BrokerAuthOption;
326
338
  config: PipelineConfig;
327
339
  eventAuthSecretKey?: string;
328
340
  eventAuthSecretName?: string;
@@ -1,3 +1,4 @@
1
+ import { brokerAuthOptionSchema } from "./broker-auth.js";
1
2
  import { buildRunnerCommandPayload, runnerDeliverySchema, runnerHookPolicySchema, runnerRepositoryContextSchema, runnerRunIdentitySchema, runnerTaskSchema } from "./runner-command-contract.js";
2
3
  import { normalizeRunnerRepositoryForSubmit } from "./git-remote-url.js";
3
4
  import { compileScheduleArtifact, generateScheduleArtifact, parseScheduleArtifact } from "./planning/generate.js";
@@ -80,6 +81,7 @@ const mokaSubmitBaseOptionsSchema = z.object({
80
81
  kubeconfigPath: z.string().min(1).optional(),
81
82
  name: z.string().min(1).optional(),
82
83
  namespace: z.string().min(1).optional(),
84
+ brokerAuth: brokerAuthOptionSchema.optional(),
83
85
  opencodeAuthSecretName: z.string().min(1).optional(),
84
86
  opencodeOpenaiAccountsSecretName: z.string().min(1).optional(),
85
87
  repository: runnerRepositoryContextSchema.optional(),
@@ -305,6 +307,7 @@ function workflowSubmitOptions(options) {
305
307
  kubeconfigPath: options.kubeconfigPath,
306
308
  name: options.name,
307
309
  namespace: requireSubmitOption(options.namespace, "namespace"),
310
+ brokerAuth: options.brokerAuth,
308
311
  opencodeAuthSecretName: options.opencodeAuthSecretName,
309
312
  opencodeOpenaiAccountsSecretName: options.opencodeOpenaiAccountsSecretName,
310
313
  serviceAccountName: options.serviceAccountName
@@ -1,3 +1,5 @@
1
+ import { applyCodexBrokerProvider, applyOpencodeBrokerProvider, renderOpencodeBrokerAuthJson, resolveBrokerCredentials } from "../broker-auth.js";
2
+ import { resolveHarnessTarget } from "../install-commands/shared.js";
1
3
  import { isRecord } from "../safe-json.js";
2
4
  import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
5
  import { homedir } from "node:os";
@@ -21,12 +23,51 @@ const WRITABLE_OPENCODE_CREDENTIAL_FILES = [{
21
23
  stagedPath: join(OPENCODE_AUTH_STAGING_DIR, AUTH_FILE_NAME)
22
24
  }];
23
25
  /**
24
- * Copy each staged opencode credential secret to its writable live path, then
25
- * sync the account pool's active openai token into auth.json's openai entry.
26
- * Only files whose staged source exists are copied (local dev / tests / configs
27
- * without a given secret keep whatever store is already present).
26
+ * Prepare codex + opencode runner credentials. In broker mode, writes the
27
+ * broker provider config and api-key and skips the legacy pool staging. In
28
+ * legacy mode, copies each staged secret to its writable live path and syncs
29
+ * the pool's active openai token into auth.json.
28
30
  */
29
31
  function prepareOpencodeCredentials(options = {}) {
32
+ const broker = options.broker === void 0 ? resolveBrokerCredentials() : options.broker;
33
+ if (broker) return {
34
+ brokerConfigured: configureBrokerCredentials(broker, options.brokerPaths),
35
+ copied: [],
36
+ hostOpenaiTokenSynced: false
37
+ };
38
+ return {
39
+ brokerConfigured: [],
40
+ ...prepareLegacyPoolCredentials(options)
41
+ };
42
+ }
43
+ function defaultBrokerConfigPaths() {
44
+ return {
45
+ codexConfigPath: resolveHarnessTarget(".codex/config.toml"),
46
+ opencodeAuthPath: join(homedir(), ".local", "share", "opencode", AUTH_FILE_NAME),
47
+ opencodeConfigPath: resolveHarnessTarget(".opencode/opencode.json")
48
+ };
49
+ }
50
+ function configureBrokerCredentials(broker, pathsOverride) {
51
+ const paths = pathsOverride ?? defaultBrokerConfigPaths();
52
+ const configured = [];
53
+ writeFileEnsured(paths.opencodeAuthPath, renderOpencodeBrokerAuthJson(broker), 384);
54
+ configured.push(basename(paths.opencodeAuthPath));
55
+ writeFileEnsured(paths.codexConfigPath, applyCodexBrokerProvider(readIfExists(paths.codexConfigPath), broker));
56
+ configured.push(basename(paths.codexConfigPath));
57
+ const opencodeConfig = applyOpencodeBrokerProvider(readIfExists(paths.opencodeConfigPath), broker);
58
+ if ("error" in opencodeConfig) throw new Error(`Cannot configure opencode broker provider: ${opencodeConfig.error}`);
59
+ writeFileEnsured(paths.opencodeConfigPath, opencodeConfig.content);
60
+ configured.push(basename(paths.opencodeConfigPath));
61
+ return configured;
62
+ }
63
+ function readIfExists(path) {
64
+ return existsSync(path) ? readFileSync(path, "utf8") : void 0;
65
+ }
66
+ function writeFileEnsured(path, content, mode) {
67
+ mkdirSync(dirname(path), { recursive: true });
68
+ writeFileSync(path, content, mode === void 0 ? void 0 : { mode });
69
+ }
70
+ function prepareLegacyPoolCredentials(options) {
30
71
  const home = homedir();
31
72
  const files = options.files ?? WRITABLE_OPENCODE_CREDENTIAL_FILES.map((file) => ({
32
73
  destPath: join(home, ...file.destFromHome),
@@ -106,6 +106,7 @@ function runRunnerCommandEffect(options, runtime) {
106
106
  }, "opencode.credentials.prepare start");
107
107
  const credentialsPrep = yield* io.prepareOpencodeCredentials();
108
108
  logger.info({
109
+ brokerConfigured: credentialsPrep.brokerConfigured,
109
110
  copied: credentialsPrep.copied,
110
111
  hostOpenaiTokenSynced: credentialsPrep.hostOpenaiTokenSynced,
111
112
  phase: "opencode.credentials.prepare",
package/package.json CHANGED
@@ -128,7 +128,7 @@
128
128
  "prepack": "bun run build:cli"
129
129
  },
130
130
  "type": "module",
131
- "version": "3.12.4",
131
+ "version": "3.13.0",
132
132
  "description": "Config-driven multi-agent pipeline runner for repository work",
133
133
  "main": "./dist/index.js",
134
134
  "types": "./dist/index.d.ts",