@oisincoveney/pipeline 3.12.3 → 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.
package/README.md CHANGED
@@ -45,10 +45,10 @@ moka init
45
45
 
46
46
  `moka init` installs or refreshes the whole per-machine harness in one step:
47
47
  the package's default skills, generated host command surfaces, the singleton
48
- `pipeline-gateway` MCP entry, copied hook files from the private
49
- `oisin-ee/agent-hooks` repository, and global instruction files. OpenCode is the
50
- package default runtime. The command does not create repo-local `.pipeline`
51
- config files.
48
+ `pipeline-gateway` MCP entry, copied hook files from private
49
+ `oisin-ee/agent/hooks`, and global instruction files from `oisin-ee/agent/rules`.
50
+ OpenCode is the package default runtime. The command does not create repo-local
51
+ `.pipeline` config files.
52
52
 
53
53
  The default MCP gateway can run locally or point at the hosted Momokaya gateway.
54
54
  Set `PIPELINE_MCP_GATEWAY_AUTHORIZATION` to the full HTTP `Authorization` header
@@ -59,7 +59,7 @@ export PIPELINE_MCP_GATEWAY_AUTHORIZATION="Basic $(printf '%s' 'user:password' |
59
59
  ```
60
60
 
61
61
  Verify the generated harness (commands, hooks, rules) is current after package
62
- upgrades or edits to `oisin-ee/agent-hooks`, without writing anything:
62
+ upgrades or edits to `oisin-ee/agent`, without writing anything:
63
63
 
64
64
  ```shell
65
65
  moka init --check
@@ -0,0 +1,7 @@
1
+ //#region src/agent-assets.ts
2
+ const AGENT_ASSET_SOURCE = "oisin-ee/agent";
3
+ const AGENT_SKILL_SOURCE = "oisin-ee/agent/skills";
4
+ const AGENT_HOOKS_DIR = "hooks";
5
+ const AGENT_RULES_DIR = "rules";
6
+ //#endregion
7
+ export { AGENT_ASSET_SOURCE, AGENT_HOOKS_DIR, AGENT_RULES_DIR, AGENT_SKILL_SOURCE };
@@ -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",
@@ -61,7 +61,7 @@ function missingFileReferenceWarning(path, value) {
61
61
  }
62
62
  function missingFileReferenceMessage(path, value) {
63
63
  const base = `${path} references missing file '${value}'`;
64
- if (path.startsWith("skills.") && value.startsWith(".agents/skills/")) return `${base}. Run \`moka init\` to install project-local skills with \`npx --yes skills add oisin-ee/skills\`.`;
64
+ if (path.startsWith("skills.") && value.startsWith(".agents/skills/")) return `${base}. Run \`moka init\` to install project-local skills with \`npx --yes skills add oisin-ee/agent/skills\`.`;
65
65
  return base;
66
66
  }
67
67
  function resolveLintPathReference(projectRoot, ref) {
@@ -233,8 +233,8 @@ declare const configSchema: z.ZodObject<{
233
233
  policy: z.ZodOptional<z.ZodObject<{
234
234
  commands: z.ZodOptional<z.ZodEnum<{
235
235
  allow: "allow";
236
- deny: "deny";
237
236
  "trusted-only": "trusted-only";
237
+ deny: "deny";
238
238
  }>>;
239
239
  modules: z.ZodOptional<z.ZodEnum<{
240
240
  allow: "allow";
@@ -262,8 +262,8 @@ declare const configSchema: z.ZodObject<{
262
262
  global: "global";
263
263
  }>>;
264
264
  mode: z.ZodEnum<{
265
- local: "local";
266
265
  hosted: "hosted";
266
+ local: "local";
267
267
  }>;
268
268
  provider: z.ZodLiteral<"toolhive">;
269
269
  authorization_env: z.ZodDefault<z.ZodString>;
@@ -306,10 +306,10 @@ declare const configSchema: z.ZodObject<{
306
306
  }, z.core.$strict>>;
307
307
  output: z.ZodOptional<z.ZodObject<{
308
308
  format: z.ZodEnum<{
309
- json_schema: "json_schema";
310
309
  text: "text";
311
310
  json: "json";
312
311
  jsonl: "jsonl";
312
+ json_schema: "json_schema";
313
313
  }>;
314
314
  repair: z.ZodOptional<z.ZodObject<{
315
315
  enabled: z.ZodOptional<z.ZodBoolean>;
@@ -385,10 +385,10 @@ declare const configSchema: z.ZodObject<{
385
385
  disabled: "disabled";
386
386
  }>>>;
387
387
  output_formats: z.ZodOptional<z.ZodArray<z.ZodEnum<{
388
- json_schema: "json_schema";
389
388
  text: "text";
390
389
  json: "json";
391
390
  jsonl: "jsonl";
391
+ json_schema: "json_schema";
392
392
  }>>>;
393
393
  rules: z.ZodOptional<z.ZodBoolean>;
394
394
  skills: z.ZodOptional<z.ZodBoolean>;
@@ -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,5 +1,6 @@
1
1
  import { applyJsonEdit, ensureTrailingNewline, parseJsonRecord } from "./json-config-merge.js";
2
2
  import { resolveHarnessTarget } from "./install-commands/shared.js";
3
+ import { AGENT_ASSET_SOURCE, AGENT_HOOKS_DIR } from "./agent-assets.js";
3
4
  import { existsSync, readFileSync, statSync } from "node:fs";
4
5
  import { tmpdir } from "node:os";
5
6
  import { dirname, join, relative } from "node:path";
@@ -7,7 +8,7 @@ import { execa } from "execa";
7
8
  import { createHash } from "node:crypto";
8
9
  import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
9
10
  //#region src/install-hooks.ts
10
- const DEFAULT_HOOK_INSTALL_SOURCE = "oisin-ee/agent-hooks";
11
+ const DEFAULT_HOOK_INSTALL_SOURCE = AGENT_ASSET_SOURCE;
11
12
  const HOOK_HOSTS = [
12
13
  "claude-code",
13
14
  "codex",
@@ -19,6 +20,7 @@ const HOST_TARGET_ROOT = {
19
20
  codex: ".codex",
20
21
  opencode: ".opencode"
21
22
  };
23
+ const NON_HOOK_OWNED_TARGETS = new Set([".opencode/opencode.json"]);
22
24
  function hashContent(content) {
23
25
  return createHash("sha256").update(content).digest("hex");
24
26
  }
@@ -63,8 +65,8 @@ async function cloneHookRepository(targetDir) {
63
65
  ], { stdio: "inherit" });
64
66
  }
65
67
  async function withHookSource(useSource) {
66
- const parent = await mkdtemp(join(tmpdir(), "moka-agent-hooks-"));
67
- const source = join(parent, "agent-hooks");
68
+ const parent = await mkdtemp(join(tmpdir(), "moka-agent-"));
69
+ const source = join(parent, "agent");
68
70
  try {
69
71
  await cloneHookRepository(source);
70
72
  return await useSource(source);
@@ -86,17 +88,17 @@ async function listFiles(root) {
86
88
  }
87
89
  async function sourceHookFiles(source) {
88
90
  return (await Promise.all(HOOK_HOSTS.map(async (host) => {
89
- const hostRoot = join(source, host);
90
- return (await listFiles(hostRoot)).map((file) => {
91
+ const hostRoot = join(source, AGENT_HOOKS_DIR, host);
92
+ return (await listFiles(hostRoot)).flatMap((file) => {
91
93
  const relativePath = relative(hostRoot, file).replaceAll("\\", "/");
92
94
  const content = readFileSync(file);
93
95
  const path = `${HOST_TARGET_ROOT[host]}/${relativePath}`;
94
- return {
96
+ return isHookOwnedTarget(path) ? [{
95
97
  content,
96
98
  hash: targetIdentityHash(path, content),
97
99
  host,
98
100
  path
99
- };
101
+ }] : [];
100
102
  });
101
103
  }))).flat().sort((a, b) => a.path.localeCompare(b.path));
102
104
  }
@@ -110,6 +112,9 @@ function emptyManifest() {
110
112
  version: 1
111
113
  };
112
114
  }
115
+ function isHookOwnedTarget(path) {
116
+ return !NON_HOOK_OWNED_TARGETS.has(path);
117
+ }
113
118
  function readManifest(host) {
114
119
  const path = manifestPath(host);
115
120
  if (!existsSync(path)) return emptyManifest();
@@ -153,18 +158,22 @@ function planFiles(files, force, manifests) {
153
158
  function planObsoleteFiles(desiredPaths, force, manifests) {
154
159
  const obsolete = [];
155
160
  for (const [host, manifest] of manifests) for (const [path, entry] of Object.entries(manifest.files)) {
156
- if (desiredPaths.has(path)) continue;
157
- const target = targetPath(path);
158
- if (!existsSync(target)) continue;
159
- const currentHash = hashContent(readFileSync(target));
160
- obsolete.push({
161
- action: force || currentHash === entry.hash ? "delete" : "conflict",
162
- host,
163
- path
164
- });
161
+ const planned = planObsoleteFile(host, path, entry, force, desiredPaths);
162
+ if (planned) obsolete.push(planned);
165
163
  }
166
164
  return obsolete.sort((a, b) => a.path.localeCompare(b.path));
167
165
  }
166
+ function planObsoleteFile(host, path, entry, force, desiredPaths) {
167
+ if (desiredPaths.has(path) || !isHookOwnedTarget(path)) return;
168
+ const target = targetPath(path);
169
+ if (!existsSync(target)) return;
170
+ const currentHash = hashContent(readFileSync(target));
171
+ return {
172
+ action: force || currentHash === entry.hash ? "delete" : "conflict",
173
+ host,
174
+ path
175
+ };
176
+ }
168
177
  async function writePlannedFile(file) {
169
178
  if (file.action === "conflict" || file.action === "unchanged") return;
170
179
  const target = targetPath(file.path);
@@ -1,9 +1,10 @@
1
+ import { AGENT_ASSET_SOURCE, AGENT_RULES_DIR } from "./agent-assets.js";
1
2
  import { homedir, tmpdir } from "node:os";
2
3
  import { join } from "node:path";
3
4
  import { execa } from "execa";
4
5
  import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
5
6
  //#region src/install-rules.ts
6
- const DEFAULT_RULES_INSTALL_SOURCE = "oisin-ee/rules";
7
+ const DEFAULT_RULES_INSTALL_SOURCE = AGENT_ASSET_SOURCE;
7
8
  const RULESYNC_PACKAGE = "rulesync@8.30.1";
8
9
  const RULESYNC_TARGETS = [
9
10
  "claudecode",
@@ -23,8 +24,8 @@ async function cloneRulesRepository(targetDir) {
23
24
  }
24
25
  async function withRulesSource(sourceOverride, useSource) {
25
26
  if (sourceOverride !== void 0) return useSource(sourceOverride);
26
- const parent = await mkdtemp(join(tmpdir(), "moka-rules-"));
27
- const source = join(parent, "rules");
27
+ const parent = await mkdtemp(join(tmpdir(), "moka-agent-rules-"));
28
+ const source = join(parent, "agent");
28
29
  try {
29
30
  await cloneRulesRepository(source);
30
31
  return await useSource(source);
@@ -52,7 +53,7 @@ async function defaultRulesyncRunner(args, opts) {
52
53
  }
53
54
  }
54
55
  async function buildRootRule(source) {
55
- const rulesDir = join(source, "rules");
56
+ const rulesDir = join(source, AGENT_RULES_DIR);
56
57
  let entries = [];
57
58
  try {
58
59
  entries = (await readdir(rulesDir, { withFileTypes: true })).filter((d) => d.isFile() && d.name.endsWith(".md")).map((d) => d.name).sort((a, b) => a.localeCompare(b));
@@ -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,17 +1,18 @@
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
 
5
6
  //#region src/moka-submit.d.ts
6
7
  declare const mokaSubmitDirectHooksSchema: z.ZodRecord<z.ZodEnum<{
7
8
  "workflow.start": "workflow.start";
8
- "node.finish": "node.finish";
9
- "node.start": "node.start";
10
9
  "workflow.success": "workflow.success";
11
10
  "workflow.failure": "workflow.failure";
12
11
  "workflow.complete": "workflow.complete";
12
+ "node.start": "node.start";
13
13
  "node.success": "node.success";
14
14
  "node.error": "node.error";
15
+ "node.finish": "node.finish";
15
16
  "gate.failure": "gate.failure";
16
17
  }> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
17
18
  failure: z.ZodDefault<z.ZodEnum<{
@@ -98,13 +99,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
98
99
  }, z.core.$strict>>;
99
100
  hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
100
101
  "workflow.start": "workflow.start";
101
- "node.finish": "node.finish";
102
- "node.start": "node.start";
103
102
  "workflow.success": "workflow.success";
104
103
  "workflow.failure": "workflow.failure";
105
104
  "workflow.complete": "workflow.complete";
105
+ "node.start": "node.start";
106
106
  "node.success": "node.success";
107
107
  "node.error": "node.error";
108
+ "node.finish": "node.finish";
108
109
  "gate.failure": "gate.failure";
109
110
  }> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
110
111
  failure: z.ZodDefault<z.ZodEnum<{
@@ -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<{
@@ -216,13 +222,13 @@ declare const mokaSubmitOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
216
222
  }, z.core.$strict>>;
217
223
  hooks: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
218
224
  "workflow.start": "workflow.start";
219
- "node.finish": "node.finish";
220
- "node.start": "node.start";
221
225
  "workflow.success": "workflow.success";
222
226
  "workflow.failure": "workflow.failure";
223
227
  "workflow.complete": "workflow.complete";
228
+ "node.start": "node.start";
224
229
  "node.success": "node.success";
225
230
  "node.error": "node.error";
231
+ "node.finish": "node.finish";
226
232
  "gate.failure": "gate.failure";
227
233
  }> & z.core.$partial, z.ZodDiscriminatedUnion<[z.ZodObject<{
228
234
  failure: z.ZodDefault<z.ZodEnum<{
@@ -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,9 +1,10 @@
1
+ import { AGENT_SKILL_SOURCE } from "./agent-assets.js";
1
2
  import { installCommands } from "./install-commands.js";
2
3
  import { installHooks } from "./install-hooks.js";
3
4
  import { installRules } from "./install-rules.js";
4
5
  import { execa } from "execa";
5
6
  //#region src/pipeline-init.ts
6
- const DEFAULT_SKILL_INSTALL_SOURCE = "oisin-ee/skills";
7
+ const DEFAULT_SKILL_INSTALL_SOURCE = AGENT_SKILL_SOURCE;
7
8
  const SKILL_INSTALL_AGENT_ARGS = [
8
9
  "--agent",
9
10
  "opencode",
@@ -84,7 +85,7 @@ function formatPipelineInitResult(result, mode = {}) {
84
85
  const copy = INIT_RESULT_COPY[initResultMode(mode)];
85
86
  return [
86
87
  copy.headline,
87
- "per-machine harness globally (user/global skills + ~/.claude, ~/.config/opencode, ~/.codex); global instruction files generated via rulesync from oisin-ee/rules; inherited by every repo with no per-repo copy",
88
+ "per-machine harness globally (user/global skills + ~/.claude, ~/.config/opencode, ~/.codex); global instruction files generated via rulesync from oisin-ee/agent/rules; inherited by every repo with no per-repo copy",
88
89
  ...result.files.map((path) => `${copy.fileVerb} ${path}`),
89
90
  copy.footer
90
91
  ].join("\n");
@@ -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",
@@ -11,8 +11,8 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
11
11
  runId: z.ZodString;
12
12
  sequence: z.ZodNumber;
13
13
  type: z.ZodEnum<{
14
- "workflow.planned": "workflow.planned";
15
14
  "workflow.start": "workflow.start";
15
+ "workflow.planned": "workflow.planned";
16
16
  }>;
17
17
  workflowPlan: z.ZodObject<{
18
18
  edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -58,10 +58,10 @@ declare const runnerEventRecordSchema: z.ZodUnion<readonly [z.ZodObject<{
58
58
  }>;
59
59
  }, z.core.$strip>;
60
60
  type: z.ZodEnum<{
61
+ "node.start": "node.start";
62
+ "node.finish": "node.finish";
61
63
  "agent.finish": "agent.finish";
62
64
  "agent.start": "agent.start";
63
- "node.finish": "node.finish";
64
- "node.start": "node.start";
65
65
  }>;
66
66
  }, z.core.$strip>, z.ZodObject<{
67
67
  at: z.ZodOptional<z.ZodString>;
@@ -256,8 +256,8 @@ declare const runnerEventBatchSchema: z.ZodObject<{
256
256
  runId: z.ZodString;
257
257
  sequence: z.ZodNumber;
258
258
  type: z.ZodEnum<{
259
- "workflow.planned": "workflow.planned";
260
259
  "workflow.start": "workflow.start";
260
+ "workflow.planned": "workflow.planned";
261
261
  }>;
262
262
  workflowPlan: z.ZodObject<{
263
263
  edges: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -303,10 +303,10 @@ declare const runnerEventBatchSchema: z.ZodObject<{
303
303
  }>;
304
304
  }, z.core.$strip>;
305
305
  type: z.ZodEnum<{
306
+ "node.start": "node.start";
307
+ "node.finish": "node.finish";
306
308
  "agent.finish": "agent.finish";
307
309
  "agent.start": "agent.start";
308
- "node.finish": "node.finish";
309
- "node.start": "node.start";
310
310
  }>;
311
311
  }, z.core.$strip>, z.ZodObject<{
312
312
  at: z.ZodOptional<z.ZodString>;
@@ -29,14 +29,10 @@ function executeOpencodeSession(deps, plan, options) {
29
29
  function boundByAgentTimeout(plan) {
30
30
  return (effect) => {
31
31
  const timeoutMs = plan.timeoutMs;
32
- process.stderr.write(`[agent-timeout] wired nodeId=${plan.nodeId} timeoutMs=${String(timeoutMs)}\n`);
33
32
  if (!timeoutMs || timeoutMs <= 0) return effect;
34
33
  return Effect.timeoutFail(Effect.disconnect(effect), {
35
34
  duration: Duration.millis(timeoutMs),
36
- onTimeout: () => {
37
- process.stderr.write(`[agent-timeout] FIRED nodeId=${plan.nodeId} after ${timeoutMs}ms\n`);
38
- return /* @__PURE__ */ new Error(`agent session timed out after ${timeoutMs}ms`);
39
- }
35
+ onTimeout: () => /* @__PURE__ */ new Error(`agent session timed out after ${timeoutMs}ms`)
40
36
  });
41
37
  };
42
38
  }
@@ -152,7 +152,7 @@ receive explicit grants:
152
152
  - `output`: text, JSON, JSONL, or JSON Schema output.
153
153
 
154
154
  Default skills resolve from project-installed skill files created by
155
- `moka init` via `npx --yes skills add oisin-ee/skills`:
155
+ `moka init` via `npx --yes skills add oisin-ee/agent/skills`:
156
156
 
157
157
  ```yaml
158
158
  skills:
@@ -164,14 +164,13 @@ Project-authored skill and rule paths resolve from the project root and must
164
164
  exist for runtime use. If default skill files are missing, run `moka init` to
165
165
  install them before executing workflows.
166
166
 
167
- Default agent hooks are copied by `moka init` from the private
168
- `oisin-ee/agent-hooks` repository. That source repository has one canonical
169
- host-level layout:
167
+ Default agent hooks are copied by `moka init` from private `oisin-ee/agent`.
168
+ That source repository has one canonical hook layout:
170
169
 
171
170
  ```text
172
- claude-code/
173
- codex/
174
- opencode/
171
+ hooks/claude-code/
172
+ hooks/codex/
173
+ hooks/opencode/
175
174
  ```
176
175
 
177
176
  Moka overlays those folders onto `.claude`, `.codex`, and `.opencode` for
@@ -215,7 +214,7 @@ OpenCode host resources are generated from the same profile registry:
215
214
  - `.opencode/skills/*/SKILL.md` is installed by `skills add`; Moka only
216
215
  generates agents, commands, plugins, and project config.
217
216
  - Additional manually authored OpenCode hook plugins can be copied from
218
- `oisin-ee/agent-hooks/opencode/` by `moka init`.
217
+ `oisin-ee/agent/hooks/opencode/` by `moka init`.
219
218
  - `.opencode/plugins/pipeline-goal-context.ts` projects package-owned
220
219
  continuation context into OpenCode compaction.
221
220
  - `.opencode/opencode.json` contains the gateway MCP config, enables LSP, and
@@ -128,3 +128,45 @@ The package-owned MCP inventory exposed through the ecosystem manifest includes
128
128
  Backlog, GitHub, and Neon. Repo-scoped backends must bind to
129
129
  `PIPELINE_TARGET_PATH` or the current workspace path supplied by the gateway
130
130
  configuration.
131
+
132
+ ## Browser automation backend (Steel)
133
+
134
+ The `Playwright` backend's tools (`playwright_browser_*`) are served by a
135
+ self-hosted **Steel Browser** (Chromium) pool, not a browser launched inside the
136
+ MCP pod. Microsoft's `@playwright/mcp` connects to Steel over CDP, so the tool
137
+ surface is unchanged — agents keep calling the same `playwright_browser_*` tools
138
+ through `pipeline-gateway`.
139
+
140
+ Topology (infra repo, `k8s/charts/pipeline-mcp-gateway`):
141
+
142
+ - A StatefulSet of N backend pods (`playwright.backendReplicas`, default 3). Each
143
+ pod is `mcp` (`@playwright/mcp`) + a private `steel` sidecar (its own Chrome on
144
+ `localhost:3000`) + an `auth-seed` native sidecar. One pod = one isolated,
145
+ verify-bot-authenticated browser.
146
+ - Auth: the seed runs a real headless Zitadel login and POSTs the session into
147
+ the pod's Steel (`POST /v1/sessions`); the pod stays NotReady until the first
148
+ seed lands (fail-closed — an unauthenticated browser is never served) and
149
+ re-seeds every ~3 days, inside the oauth2-proxy 7-day cookie window.
150
+
151
+ Usage:
152
+
153
+ - **One authenticated browser (default).** Call `playwright_browser_*` through
154
+ `pipeline-gateway` (`https://pipeline-mcp.momokaya.ee/mcp/`). You get a single,
155
+ pre-authenticated browser. After a gateway backend restart the vMCP client
156
+ session can drop — reconnect the MCP client (do not bounce pods).
157
+ - **N concurrent isolated browsers.** The single gateway/proxy endpoint does
158
+ **not** auto-distribute sessions across the pool — toolhive pins every session
159
+ to one backend pod (Redis session storage and scaling proxy replicas do not
160
+ change this). To use the pool concurrently, address the backend pods directly:
161
+ each pod's `@playwright/mcp` listens on port `8931` and is a full
162
+ `playwright_browser_*` endpoint (`http://<pod-ip-or-headless-dns>:8931/mcp`).
163
+ Proven: 3 concurrent per-pod sessions, each on a distinct authenticated
164
+ browser.
165
+ - Scale the pool with `playwright.backendReplicas`.
166
+
167
+ Operational notes: Steel runs as root in-pod (its bundled nginx requires it,
168
+ otherwise `nginx [emerg] chown(/var/lib/nginx/body) Operation not permitted`);
169
+ health is `GET /v1/health`; on ARM nodes set `SKIP_FINGERPRINT_INJECTION=true`;
170
+ CDP over a service-DNS host needs `--cdp-header "Host: localhost"` (Chrome's
171
+ anti-DNS-rebinding check), but the in-pod `localhost:3000` path needs no header.
172
+ See infra `INFRA-074`.
@@ -177,8 +177,8 @@ OpenBao, publish Secret values, or mutate ESO resources from this package.
177
177
 
178
178
  Installs or refreshes the whole per-machine harness in one command: the
179
179
  package's default skills, generated host-native command surfaces and MCP
180
- entries, copied agent hooks from the private `oisin-ee/agent-hooks` repository,
181
- and global instruction files generated via rulesync from `oisin-ee/rules`.
180
+ entries, copied agent hooks from private `oisin-ee/agent/hooks`, and global
181
+ instruction files generated via rulesync from `oisin-ee/agent/rules`.
182
182
  OpenCode is the package default runtime. The harness is always installed
183
183
  globally (`~/.claude`, `~/.config/opencode`, `~/.codex`); there is no `--scope`.
184
184
  `moka init` does not create repo-local `.pipeline` config files.
@@ -192,12 +192,10 @@ moka init --force # overwrite manually edited harness files
192
192
 
193
193
  `--check` and `--dry-run` write nothing and skip the network skill install.
194
194
  By default `moka init` refuses to overwrite manually edited hook or command
195
- files; `--force` overwrites them. For agent hooks, Moka clones
196
- `oisin-ee/agent-hooks`, copies files, and tracks installed hashes so later runs
197
- update unchanged owned files, delete removed owned files, and (without `--force`)
198
- refuse to clobber manual edits. The hook source repository has only host folders
199
- (`claude-code/`, `codex/`, `opencode/`); there is no source override flag and no
200
- symlink mode.
195
+ files; `--force` overwrites them. For agent hooks, Moka clones `oisin-ee/agent`,
196
+ copies files from `hooks/<host>`, and tracks installed hashes so later runs update
197
+ unchanged owned files, delete removed owned files, and (without `--force`) refuse
198
+ to clobber manual edits. There is no source override flag and no symlink mode.
201
199
 
202
200
  Use `PIPELINE_TARGET_PATH=/path/to/repo` when invoking `moka` from outside the
203
201
  target worktree.
@@ -384,9 +382,9 @@ Claude Code: /moka-quick, /moka-execute, /moka-inspect
384
382
  - `.opencode/opencode.json` with LSP, the singleton `pipeline-gateway` MCP
385
383
  server, and pinned package-selected plugins
386
384
 
387
- `moka init` also copies hook files from `oisin-ee/agent-hooks` by overlaying
388
- `opencode/`, `claude-code/`, and `codex/` onto the host config roots. Hook files
389
- are authored in the hook repo, not generated by Moka.
385
+ `moka init` also copies hook files from `oisin-ee/agent/hooks` by overlaying
386
+ `hooks/opencode/`, `hooks/claude-code/`, and `hooks/codex/` onto the host config
387
+ roots. Hook files are authored in the agent asset repo, not generated by Moka.
390
388
 
391
389
  For Claude Code, `moka init` generates `.claude/commands/moka-<entrypoint>.md`
392
390
  slash commands.
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.3",
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",