@kody-ade/kody-engine 0.4.643 → 0.4.645

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/dist/bin/kody.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "@kody-ade/kody-engine",
18
- version: "0.4.643",
18
+ version: "0.4.645",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative implementation profiles.",
20
20
  license: "MIT",
21
21
  repository: {
@@ -2334,7 +2334,7 @@ function parseCapabilityContract(raw) {
2334
2334
  if (parsed.execution !== void 0 && parsed.execution !== "agent" && parsed.execution !== "script") {
2335
2335
  throw new Error('contract.json execution must be "agent" or "script"');
2336
2336
  }
2337
- const requirements = parseCapabilityRequirements(parsed.requirements);
2337
+ const requirements = parseCapabilityRequirements(parsed.requirements, parsed.execution);
2338
2338
  const secrets = parsed.secrets === void 0 ? void 0 : Array.isArray(parsed.secrets) && parsed.secrets.every((name) => typeof name === "string" && /^[A-Z][A-Z0-9_]*$/.test(name)) ? [...new Set(parsed.secrets)] : null;
2339
2339
  if (secrets === null) {
2340
2340
  throw new Error("contract.json secrets must contain valid environment variable names");
@@ -2342,6 +2342,13 @@ function parseCapabilityContract(raw) {
2342
2342
  if (secrets && parsed.execution !== "script") {
2343
2343
  throw new Error('contract.json secrets are supported only when execution is "script"');
2344
2344
  }
2345
+ const connections = parsed.connections === void 0 ? void 0 : Array.isArray(parsed.connections) && parsed.connections.length > 0 && parsed.connections.every((id) => typeof id === "string" && /^[a-z0-9][a-z0-9_-]{0,79}$/.test(id)) ? [...new Set(parsed.connections)] : null;
2346
+ if (connections === null) {
2347
+ throw new Error("contract.json connections must contain valid Connection ids");
2348
+ }
2349
+ if (connections && parsed.execution !== "script") {
2350
+ throw new Error('contract.json connections are supported only when execution is "script"');
2351
+ }
2345
2352
  const timeoutMs = parsed.timeoutMs === void 0 ? void 0 : typeof parsed.timeoutMs === "number" && Number.isInteger(parsed.timeoutMs) && parsed.timeoutMs >= 1e3 && parsed.timeoutMs <= 6 * 60 * 60 * 1e3 ? parsed.timeoutMs : null;
2346
2353
  if (timeoutMs === null) {
2347
2354
  throw new Error("contract.json timeoutMs must be an integer from 1000 to 21600000");
@@ -2362,7 +2369,7 @@ function parseCapabilityContract(raw) {
2362
2369
  throw new Error("contract.json deliveryConfigAllowlist files must also be deliveryPathAllowlist entries");
2363
2370
  }
2364
2371
  const unsupported = Object.keys(parsed).filter(
2365
- (key) => key !== "execution" && key !== "deliveryPolicy" && key !== "deliveryPathAllowlist" && key !== "deliveryConfigAllowlist" && key !== "requirements" && key !== "secrets" && key !== "timeoutMs" && key !== "requiredSubagents" && key !== "input" && key !== "output"
2372
+ (key) => key !== "execution" && key !== "deliveryPolicy" && key !== "deliveryPathAllowlist" && key !== "deliveryConfigAllowlist" && key !== "requirements" && key !== "connections" && key !== "secrets" && key !== "timeoutMs" && key !== "requiredSubagents" && key !== "input" && key !== "output"
2366
2373
  );
2367
2374
  if (unsupported.length > 0) {
2368
2375
  throw new Error(`contract.json contains unsupported fields: ${unsupported.join(", ")}`);
@@ -2379,6 +2386,7 @@ function parseCapabilityContract(raw) {
2379
2386
  ...deliveryPathAllowlist ? { deliveryPathAllowlist } : {},
2380
2387
  ...deliveryConfigAllowlist ? { deliveryConfigAllowlist } : {},
2381
2388
  ...requirements ? { requirements } : {},
2389
+ ...connections ? { connections } : {},
2382
2390
  ...secrets ? { secrets } : {},
2383
2391
  ...timeoutMs !== void 0 ? { timeoutMs } : {},
2384
2392
  ...requiredSubagents ? { requiredSubagents } : {},
@@ -2425,11 +2433,11 @@ function parseDeliveryPathAllowlist(raw) {
2425
2433
  }
2426
2434
  return paths;
2427
2435
  }
2428
- function parseCapabilityRequirements(raw) {
2436
+ function parseCapabilityRequirements(raw, execution) {
2429
2437
  if (raw === void 0) return void 0;
2430
2438
  if (!isPlainObject(raw)) throw new Error("contract.json requirements must be an object");
2431
2439
  const unsupported = Object.keys(raw).filter(
2432
- (key) => key !== "cms" && key !== "browser" && key !== "qaCredentials" && key !== "githubTestToken" && key !== "qaAccountCredentials" && key !== "qaAccountModelSettings" && key !== "browserOnly"
2440
+ (key) => key !== "cms" && key !== "browser" && key !== "qaCredentials" && key !== "githubTestToken" && key !== "qaAccountCredentials" && key !== "qaAccountModelSettings" && key !== "browserOnly" && key !== "browserSession" && key !== "browserActions" && key !== "browserOrigins" && key !== "browserFileRoots"
2433
2441
  );
2434
2442
  if (unsupported.length > 0) {
2435
2443
  throw new Error(`contract.json requirements contains unsupported fields: ${unsupported.join(", ")}`);
@@ -2455,8 +2463,30 @@ function parseCapabilityRequirements(raw) {
2455
2463
  if (raw.browserOnly !== void 0 && typeof raw.browserOnly !== "boolean") {
2456
2464
  throw new Error("contract.json requirements.browserOnly must be boolean");
2457
2465
  }
2458
- if ((raw.qaCredentials === true || raw.githubTestToken === true || raw.qaAccountCredentials !== void 0 || raw.qaAccountModelSettings !== void 0 || raw.browserOnly === true) && raw.browser !== true) {
2459
- throw new Error("contract.json authentication requirements require browser");
2466
+ if (raw.browserSession !== void 0 && raw.browserSession !== "user") {
2467
+ throw new Error('contract.json requirements.browserSession must be "user"');
2468
+ }
2469
+ const browserActions = parseUserBrowserActions(raw.browserActions);
2470
+ const browserOrigins = parseUserBrowserOrigins(raw.browserOrigins);
2471
+ const browserFileRoots = parseUserBrowserFileRoots(raw.browserFileRoots);
2472
+ if ((raw.qaCredentials === true || raw.githubTestToken === true || raw.qaAccountCredentials !== void 0 || raw.qaAccountModelSettings !== void 0 || raw.browserOnly === true || raw.browserSession === "user") && raw.browser !== true) {
2473
+ throw new Error("contract.json protected browser requirement requires browser");
2474
+ }
2475
+ if (raw.browserSession === "user") {
2476
+ if (execution !== "agent") {
2477
+ throw new Error('contract.json requirements.browserSession "user" is supported only when execution is "agent"');
2478
+ }
2479
+ if (!browserActions?.length) {
2480
+ throw new Error("contract.json user browser requirements need browserActions");
2481
+ }
2482
+ if (!browserOrigins?.length) {
2483
+ throw new Error("contract.json user browser requirements need browserOrigins");
2484
+ }
2485
+ if (browserActions.includes("upload") && !browserFileRoots?.length) {
2486
+ throw new Error("contract.json browser upload requires browserFileRoots");
2487
+ }
2488
+ } else if (browserActions !== void 0 || browserOrigins !== void 0 || browserFileRoots !== void 0) {
2489
+ throw new Error('contract.json browserActions, browserOrigins, and browserFileRoots require browserSession "user"');
2460
2490
  }
2461
2491
  const requirements = {
2462
2492
  ...raw.cms === true ? { cms: true } : {},
@@ -2465,10 +2495,58 @@ function parseCapabilityRequirements(raw) {
2465
2495
  ...raw.githubTestToken === true ? { githubTestToken: true } : {},
2466
2496
  ...Array.isArray(raw.qaAccountCredentials) ? { qaAccountCredentials: [...new Set(raw.qaAccountCredentials)] } : {},
2467
2497
  ...isPlainObject(raw.qaAccountModelSettings) ? { qaAccountModelSettings: raw.qaAccountModelSettings } : {},
2468
- ...raw.browserOnly === true ? { browserOnly: true } : {}
2498
+ ...raw.browserOnly === true ? { browserOnly: true } : {},
2499
+ ...raw.browserSession === "user" ? { browserSession: "user" } : {},
2500
+ ...browserActions ? { browserActions } : {},
2501
+ ...browserOrigins ? { browserOrigins } : {},
2502
+ ...browserFileRoots ? { browserFileRoots } : {}
2469
2503
  };
2470
2504
  return Object.keys(requirements).length > 0 ? requirements : void 0;
2471
2505
  }
2506
+ function parseUserBrowserActions(value) {
2507
+ if (value === void 0) return void 0;
2508
+ if (!Array.isArray(value) || value.length === 0 || value.length > USER_BROWSER_ACTIONS.length || !value.every(
2509
+ (action) => typeof action === "string" && USER_BROWSER_ACTIONS.includes(action)
2510
+ )) {
2511
+ throw new Error(`contract.json requirements.browserActions must contain only ${USER_BROWSER_ACTIONS.join(", ")}`);
2512
+ }
2513
+ return [...new Set(value)];
2514
+ }
2515
+ function parseUserBrowserOrigins(value) {
2516
+ if (value === void 0) return void 0;
2517
+ if (!Array.isArray(value) || value.length === 0 || value.length > 20) {
2518
+ throw new Error("contract.json requirements.browserOrigins must be a non-empty array");
2519
+ }
2520
+ const origins = value.map((raw) => {
2521
+ if (typeof raw !== "string") throw new Error("invalid browser origin");
2522
+ let parsed;
2523
+ try {
2524
+ parsed = new URL(raw);
2525
+ } catch {
2526
+ throw new Error("invalid browser origin");
2527
+ }
2528
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.origin !== raw.replace(/\/$/, "")) {
2529
+ throw new Error("contract.json requirements.browserOrigins must contain HTTPS origins only");
2530
+ }
2531
+ return parsed.origin;
2532
+ });
2533
+ return [...new Set(origins)];
2534
+ }
2535
+ function parseUserBrowserFileRoots(value) {
2536
+ if (value === void 0) return void 0;
2537
+ if (!Array.isArray(value) || value.length === 0 || value.length > 20) {
2538
+ throw new Error("contract.json requirements.browserFileRoots must be a non-empty array");
2539
+ }
2540
+ const roots = value.map((raw) => {
2541
+ if (typeof raw !== "string") throw new Error("invalid browser file root");
2542
+ const root = raw.replaceAll("\\", "/").replace(/^\/+|\/+$/g, "");
2543
+ if (!root || root.length > 300 || root.split("/").some((part) => !part || part === "." || part === "..")) {
2544
+ throw new Error("contract.json requirements.browserFileRoots contains an unsafe path");
2545
+ }
2546
+ return root;
2547
+ });
2548
+ return [...new Set(roots)];
2549
+ }
2472
2550
  function isRegularFile(filePath) {
2473
2551
  try {
2474
2552
  const stat = fs8.lstatSync(filePath);
@@ -2545,6 +2623,7 @@ function parseWorkflowStep(value) {
2545
2623
  const target = stringField(raw.target);
2546
2624
  const delivery = stringField(raw.delivery);
2547
2625
  const targetFact = stringField(raw.targetFact ?? raw.target_fact);
2626
+ const approval = stringField(raw.approval);
2548
2627
  const timeoutSeconds = typeof raw.timeoutSeconds === "number" && Number.isInteger(raw.timeoutSeconds) && raw.timeoutSeconds > 0 && raw.timeoutSeconds <= 3600 ? raw.timeoutSeconds : void 0;
2549
2628
  const hasInput = Object.hasOwn(raw, "input");
2550
2629
  const inputs = parseWorkflowInputBindings(raw.inputs);
@@ -2562,6 +2641,7 @@ function parseWorkflowStep(value) {
2562
2641
  ...targetFact ? { targetFact } : {},
2563
2642
  ...reason ? { reason } : {},
2564
2643
  ...timeoutSeconds ? { timeoutSeconds } : {},
2644
+ ...approval === "required" ? { approval: "required" } : {},
2565
2645
  ...next ? { next } : {},
2566
2646
  ...isPlainObject(raw.runWhen) ? { runWhen: raw.runWhen } : {},
2567
2647
  ...stringList(raw.continueOn ?? raw.continue_on).length > 0 ? { continueOn: stringList(raw.continueOn ?? raw.continue_on) } : {},
@@ -2636,7 +2716,7 @@ function isSafeSlug(value) {
2636
2716
  function isSafeStepId(value) {
2637
2717
  return /^[A-Za-z][A-Za-z0-9_-]*$/.test(value) && !value.includes("..");
2638
2718
  }
2639
- var CAPABILITY_BODY_FILE, CAPABILITY_CONTRACT_FILE, CAPABILITY_PROFILE_FILE, CANONICAL_CAPABILITY_BODY_FILE, CANONICAL_CAPABILITY_DEFINITION_FILE;
2719
+ var CAPABILITY_BODY_FILE, CAPABILITY_CONTRACT_FILE, CAPABILITY_PROFILE_FILE, CANONICAL_CAPABILITY_BODY_FILE, CANONICAL_CAPABILITY_DEFINITION_FILE, USER_BROWSER_ACTIONS;
2640
2720
  var init_capabilityFolders = __esm({
2641
2721
  "src/capabilityFolders.ts"() {
2642
2722
  "use strict";
@@ -2645,6 +2725,7 @@ var init_capabilityFolders = __esm({
2645
2725
  CAPABILITY_PROFILE_FILE = CAPABILITY_BODY_FILE;
2646
2726
  CANONICAL_CAPABILITY_BODY_FILE = "capability.md";
2647
2727
  CANONICAL_CAPABILITY_DEFINITION_FILE = "definition.json";
2728
+ USER_BROWSER_ACTIONS = ["navigate", "click", "fill", "upload", "scroll", "wait"];
2648
2729
  }
2649
2730
  });
2650
2731
 
@@ -3165,6 +3246,19 @@ async function readRuntimeSecretFromKody(name, env = process.env) {
3165
3246
  const body = await response.json();
3166
3247
  return typeof body.value === "string" ? body.value : null;
3167
3248
  }
3249
+ async function readRuntimeConnectionFromKody(id, env = process.env) {
3250
+ const token = await githubOidcToken(env);
3251
+ const response = await fetch(`${resolveKodyApiUrl(env)}/api/kody/engine/connection`, {
3252
+ method: "POST",
3253
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
3254
+ body: JSON.stringify({ id }),
3255
+ signal: AbortSignal.timeout(15e3)
3256
+ });
3257
+ if (response.status === 404) return null;
3258
+ if (!response.ok) throw new Error(`Kody Connection request failed (${response.status})`);
3259
+ const body = await response.json();
3260
+ return body.connection ?? null;
3261
+ }
3168
3262
  async function writeRuntimeSecretsToKody(secrets, env = process.env) {
3169
3263
  const token = await githubOidcToken(env);
3170
3264
  const response = await fetch(`${resolveKodyApiUrl(env)}/api/kody/engine/secret`, {
@@ -5271,6 +5365,17 @@ function validateWorkflow(value, options = {}) {
5271
5365
  "workflow step timeoutSeconds must be an integer from 1 to 3600"
5272
5366
  );
5273
5367
  }
5368
+ if (step.approval !== void 0 && step.approval !== "required") {
5369
+ issue(issues, "invalid_step_approval", `${base}.approval`, 'workflow step approval must be "required"');
5370
+ }
5371
+ if (step.approval === "required" && !text(step.id)) {
5372
+ issue(
5373
+ issues,
5374
+ "approval_requires_step_id",
5375
+ `${base}.approval`,
5376
+ "an approval-gated workflow step must have a stable id"
5377
+ );
5378
+ }
5274
5379
  validateInputBindings(
5275
5380
  step.inputs,
5276
5381
  `${base}.inputs`,
@@ -5564,6 +5669,7 @@ var init_workflowValidation = __esm({
5564
5669
  "targetFact",
5565
5670
  "reason",
5566
5671
  "timeoutSeconds",
5672
+ "approval",
5567
5673
  "next",
5568
5674
  "runWhen",
5569
5675
  "continueOn",
@@ -8184,7 +8290,9 @@ import * as os4 from "os";
8184
8290
  import * as path27 from "path";
8185
8291
  async function checkLitellmHealth(url) {
8186
8292
  try {
8187
- const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
8293
+ const response = await fetch(`${url.replace(/\/+$/, "")}/health/liveliness`, {
8294
+ signal: AbortSignal.timeout(3e3)
8295
+ });
8188
8296
  return response.ok;
8189
8297
  } catch {
8190
8298
  return false;
@@ -17189,6 +17297,7 @@ var init_loadSimpleCapability = __esm({
17189
17297
  if (requiredSubagents.length > 0) ctx.data.requiredSubagents = requiredSubagents;
17190
17298
  if (capability.contract?.execution === "script") {
17191
17299
  ctx.data.capabilityScriptPath = path43.join(capability.dir, "tools", "run.sh");
17300
+ ctx.data.capabilityConnectionIds = capability.contract.connections ?? [];
17192
17301
  ctx.data.capabilitySecretNames = capability.contract.secrets ?? [];
17193
17302
  ctx.data.capabilityScriptTimeoutMs = capability.contract.timeoutMs;
17194
17303
  }
@@ -19461,6 +19570,9 @@ var init_prepareSimpleCapabilityRuntime = __esm({
19461
19570
  prepareSimpleCapabilityRuntime = async (ctx, profile) => {
19462
19571
  const requirements = requirementsFrom(ctx);
19463
19572
  if (!requirements.browser) return;
19573
+ if (requirements.browserSession === "user") {
19574
+ throw new Error("Capability requires the Dashboard user browser session and cannot run in CI");
19575
+ }
19464
19576
  configureBrowser(ctx, profile, requirements);
19465
19577
  if (requirements.qaCredentials) {
19466
19578
  await loadQaContext(ctx, profile);
@@ -21230,6 +21342,33 @@ var init_runScheduledImplementationTick = __esm({
21230
21342
  }
21231
21343
  });
21232
21344
 
21345
+ // src/scripts/runtimeConnections.ts
21346
+ async function resolveRuntimeConnections(ids, declaredSecrets, load = readRuntimeConnectionFromKody) {
21347
+ const requested = Array.isArray(ids) ? [...new Set(ids.filter((id) => typeof id === "string" && /^[a-z0-9][a-z0-9-]{0,63}$/.test(id)))] : [];
21348
+ const allowedSecrets = new Set(
21349
+ Array.isArray(declaredSecrets) ? declaredSecrets.filter((name) => typeof name === "string") : []
21350
+ );
21351
+ const connections = [];
21352
+ for (const id of requested) {
21353
+ const connection = await load(id);
21354
+ if (!connection) throw new Error(`Connection ${id} was not found`);
21355
+ if (connection.status !== "connected") throw new Error(`Connection ${id} is not connected`);
21356
+ for (const secretName of Object.values(connection.credentialRefs)) {
21357
+ if (!allowedSecrets.has(secretName)) {
21358
+ throw new Error(`Connection ${id} credential ${secretName} is not allowlisted by the Capability`);
21359
+ }
21360
+ }
21361
+ connections.push(connection);
21362
+ }
21363
+ return connections;
21364
+ }
21365
+ var init_runtimeConnections = __esm({
21366
+ "src/scripts/runtimeConnections.ts"() {
21367
+ "use strict";
21368
+ init_kody_api_client();
21369
+ }
21370
+ });
21371
+
21233
21372
  // src/scripts/runSimpleCapabilityScript.ts
21234
21373
  import { spawnSync as spawnSync3 } from "child_process";
21235
21374
  import * as fs51 from "fs";
@@ -21252,6 +21391,7 @@ var init_runSimpleCapabilityScript = __esm({
21252
21391
  "src/scripts/runSimpleCapabilityScript.ts"() {
21253
21392
  "use strict";
21254
21393
  init_capabilityResult();
21394
+ init_runtimeConnections();
21255
21395
  init_runtimeSecrets();
21256
21396
  init_tickShellRunner();
21257
21397
  DEFAULT_SCRIPT_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -21265,6 +21405,14 @@ var init_runSimpleCapabilityScript = __esm({
21265
21405
  return;
21266
21406
  }
21267
21407
  const capabilityEnvironment = isStringRecord(ctx.data.capabilityEnvironment) ? ctx.data.capabilityEnvironment : {};
21408
+ let connections;
21409
+ try {
21410
+ connections = await resolveRuntimeConnections(ctx.data.capabilityConnectionIds, ctx.data.capabilitySecretNames);
21411
+ } catch (error) {
21412
+ ctx.output.exitCode = 78;
21413
+ ctx.output.reason = error instanceof Error ? error.message : "Capability Connection loading failed";
21414
+ return;
21415
+ }
21268
21416
  const capabilitySecrets = await resolveRuntimeSecrets(ctx.data.capabilitySecretNames, ctx);
21269
21417
  for (const warning of capabilitySecrets.warnings) {
21270
21418
  process.stderr.write(`\u2192 kody: WARNING ${warning}
@@ -21276,7 +21424,11 @@ var init_runSimpleCapabilityScript = __esm({
21276
21424
  env: {
21277
21425
  ...buildTickChildEnv(process.env, false),
21278
21426
  ...capabilitySecrets.environment,
21279
- ...capabilityEnvironment
21427
+ ...capabilityEnvironment,
21428
+ ...connections.length > 0 ? {
21429
+ KODY_CONNECTIONS_JSON: JSON.stringify(connections),
21430
+ ...connections.length === 1 ? { KODY_CONNECTION_JSON: JSON.stringify(connections[0]) } : {}
21431
+ } : {}
21280
21432
  },
21281
21433
  stdio: ["ignore", "pipe", "pipe"],
21282
21434
  encoding: "utf-8",
@@ -24279,7 +24431,7 @@ function workflowRunStatePath(workflowId, runId) {
24279
24431
  function parseWorkflowRunState(raw) {
24280
24432
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
24281
24433
  const state = raw;
24282
- if (state.status !== "running" && state.status !== "blocked" && state.status !== "failed" && state.status !== "done")
24434
+ if (state.status !== "running" && state.status !== "waiting-approval" && state.status !== "blocked" && state.status !== "failed" && state.status !== "done")
24283
24435
  return null;
24284
24436
  const completedStepIds = Array.isArray(state.completedStepIds) ? state.completedStepIds.filter((value) => typeof value === "string") : [];
24285
24437
  const transitionCounts = state.transitionCounts && typeof state.transitionCounts === "object" && !Array.isArray(state.transitionCounts) ? Object.fromEntries(
@@ -24299,6 +24451,7 @@ function parseWorkflowRunState(raw) {
24299
24451
  ...typeof state.currentStepId === "string" ? { currentStepId: state.currentStepId } : {},
24300
24452
  completedStepIds,
24301
24453
  transitionCounts,
24454
+ ...parseWorkflowApproval(state.approval) ? { approval: parseWorkflowApproval(state.approval) } : {},
24302
24455
  ...input ? { input: { ...input } } : {},
24303
24456
  ...typeof state.definitionHash === "string" && state.definitionHash.trim() ? { definitionHash: state.definitionHash.trim() } : {},
24304
24457
  ...steps ? { steps } : {},
@@ -24308,6 +24461,20 @@ function parseWorkflowRunState(raw) {
24308
24461
  ...typeof state.blocker === "string" ? { blocker: state.blocker } : {}
24309
24462
  };
24310
24463
  }
24464
+ function parseWorkflowApproval(value) {
24465
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
24466
+ const approval = value;
24467
+ if (typeof approval.stepId !== "string" || typeof approval.action !== "string" || typeof approval.contextHash !== "string" || approval.status !== "pending" && approval.status !== "approved" && approval.status !== "consumed")
24468
+ return void 0;
24469
+ return {
24470
+ stepId: approval.stepId,
24471
+ action: approval.action,
24472
+ contextHash: approval.contextHash,
24473
+ status: approval.status,
24474
+ ...typeof approval.approvedAt === "string" ? { approvedAt: approval.approvedAt } : {},
24475
+ ...typeof approval.approvedBy === "string" ? { approvedBy: approval.approvedBy } : {}
24476
+ };
24477
+ }
24311
24478
  function parseWorkflowSteps(value) {
24312
24479
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
24313
24480
  const steps = {};
@@ -24358,6 +24525,50 @@ var init_workflowRunState = __esm({
24358
24525
  }
24359
24526
  });
24360
24527
 
24528
+ // src/workflowStepApproval.ts
24529
+ import { createHash as createHash9 } from "crypto";
24530
+ function workflowStepApprovalContextHash(state, stepId) {
24531
+ const value = stableJson2({
24532
+ stepId,
24533
+ definitionHash: state.definitionHash ?? null,
24534
+ input: state.input ?? {},
24535
+ facts: state.facts ?? {}
24536
+ });
24537
+ return `sha256:${createHash9("sha256").update(value).digest("hex")}`;
24538
+ }
24539
+ function requireWorkflowStepApproval(state, stepId) {
24540
+ const contextHash = workflowStepApprovalContextHash(state, stepId);
24541
+ if (state.approval?.stepId === stepId && state.approval.contextHash === contextHash && state.approval.status === "approved") {
24542
+ return {
24543
+ ...state,
24544
+ status: "running",
24545
+ approval: { ...state.approval, status: "consumed" }
24546
+ };
24547
+ }
24548
+ return {
24549
+ ...state,
24550
+ status: "waiting-approval",
24551
+ approval: {
24552
+ stepId,
24553
+ action: `workflow-step:${stepId}`,
24554
+ contextHash,
24555
+ status: "pending"
24556
+ }
24557
+ };
24558
+ }
24559
+ function stableJson2(value) {
24560
+ if (Array.isArray(value)) return `[${value.map(stableJson2).join(",")}]`;
24561
+ if (value && typeof value === "object") {
24562
+ return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson2(item)}`).join(",")}}`;
24563
+ }
24564
+ return JSON.stringify(value) ?? "undefined";
24565
+ }
24566
+ var init_workflowStepApproval = __esm({
24567
+ "src/workflowStepApproval.ts"() {
24568
+ "use strict";
24569
+ }
24570
+ });
24571
+
24361
24572
  // src/job.ts
24362
24573
  var job_exports = {};
24363
24574
  __export(job_exports, {
@@ -24528,7 +24739,7 @@ async function runJob(job, base) {
24528
24739
  if (base.config && persistRun) {
24529
24740
  await upsertRunIndexRowBestEffortAsync(base.config, base.cwd, {
24530
24741
  ...parentRow,
24531
- status: result.exitCode === 0 ? "success" : "failed",
24742
+ status: result.workflowState?.status === "waiting-approval" ? "waiting" : result.exitCode === 0 ? "success" : "failed",
24532
24743
  summary: result.reason,
24533
24744
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
24534
24745
  });
@@ -24537,7 +24748,7 @@ async function runJob(job, base) {
24537
24748
  await lease?.checkpoint();
24538
24749
  await writeWorkflowRunState(base.config, base.cwd, workflowIdentity, valid.workflowRunId, result.workflowState);
24539
24750
  }
24540
- if (valid.workflowRunId && workflowIdentity && hasGitHubActionsIdentity()) {
24751
+ if (valid.workflowRunId && workflowIdentity && hasGitHubActionsIdentity() && result.workflowState?.status !== "waiting-approval") {
24541
24752
  const facts = result.workflowState?.facts ?? {};
24542
24753
  await notifyWorkflowCompleted({
24543
24754
  workflowId: workflowIdentity,
@@ -24829,6 +25040,7 @@ function initialWorkflowState(parent, workflow) {
24829
25040
  status: "done",
24830
25041
  completedStepIds: [...prior.completedStepIds],
24831
25042
  transitionCounts: { ...prior.transitionCounts },
25043
+ ...prior.approval ? { approval: { ...prior.approval } } : {},
24832
25044
  ...prior.input ? { input: { ...prior.input } } : {},
24833
25045
  ...prior.steps ? { steps: cloneWorkflowSteps(prior.steps) } : {},
24834
25046
  facts: { ...prior.facts },
@@ -24845,6 +25057,7 @@ function initialWorkflowState(parent, workflow) {
24845
25057
  ...currentStepId ? { currentStepId } : {},
24846
25058
  completedStepIds: [...prior?.completedStepIds ?? []],
24847
25059
  transitionCounts: { ...prior?.transitionCounts ?? {} },
25060
+ ...prior?.approval ? { approval: { ...prior.approval } } : {},
24848
25061
  steps: cloneWorkflowSteps(prior?.steps ?? {}),
24849
25062
  facts: {
24850
25063
  ...workflowInputContext(parent.cliArgs),
@@ -24903,6 +25116,20 @@ async function runGraphCapabilityWorkflow(parent, workflow, capability, base, ch
24903
25116
  return { ...result, exitCode: 64, reason, workflowState: state };
24904
25117
  }
24905
25118
  const label = step.action ?? step.capability;
25119
+ if (step.approval === "required") {
25120
+ const approvalState = requireWorkflowStepApproval(state, step.id);
25121
+ state.status = approvalState.status;
25122
+ state.approval = approvalState.approval;
25123
+ await checkpoint?.(state);
25124
+ if (state.status === "waiting-approval") {
25125
+ return {
25126
+ ...result,
25127
+ exitCode: 0,
25128
+ reason: `Approval required before workflow step ${step.id}`,
25129
+ workflowState: state
25130
+ };
25131
+ }
25132
+ }
24906
25133
  await checkpoint?.(state);
24907
25134
  let child;
24908
25135
  try {
@@ -25118,7 +25345,8 @@ function workflowStepToJob(step, parent, chainData, cwd) {
25118
25345
  const action = step.action ?? step.capability;
25119
25346
  const targetNumber = workflowStepTargetNumber(step, parent, chainData);
25120
25347
  const mappedInputs = resolveWorkflowStepInputs(step, chainData, cwd);
25121
- const rawArgs = mappedInputs ? { ...mappedInputs } : { ...parent.cliArgs };
25348
+ const genericCapability = usesGenericCapabilityInput(action, cwd);
25349
+ const rawArgs = mappedInputs ? { ...mappedInputs } : genericCapability ? inheritedGenericStepInput(step.capability, parent.cliArgs, cwd) : { ...parent.cliArgs };
25122
25350
  if (step.target === "pr") {
25123
25351
  if (typeof targetNumber !== "number") {
25124
25352
  throw new InvalidJobError(`workflow step ${action} needs a PR target but no prior PR URL is available`);
@@ -25128,11 +25356,11 @@ function workflowStepToJob(step, parent, chainData, cwd) {
25128
25356
  rawArgs.issue = targetNumber;
25129
25357
  }
25130
25358
  const genericInput = capabilityStepInput(
25131
- step.input ?? mappedInputs ?? chainData.workflowInput ?? genericInputFromArgs(rawArgs),
25359
+ step.input ?? mappedInputs ?? genericInputFromArgs(rawArgs),
25132
25360
  step.target,
25133
25361
  targetNumber
25134
25362
  );
25135
- const cliArgs = usesGenericCapabilityInput(action, cwd) ? genericInput === void 0 ? {} : { input: JSON.stringify(genericInput) } : filterCliArgsForStep(action, rawArgs);
25363
+ const cliArgs = genericCapability ? genericInput === void 0 ? {} : { input: JSON.stringify(genericInput) } : filterCliArgsForStep(action, rawArgs);
25136
25364
  const target = typeof targetNumber === "number" ? targetNumber : typeof parent.target === "number" ? parent.target : targetFromCliArgs(cliArgs);
25137
25365
  return {
25138
25366
  action,
@@ -25183,6 +25411,17 @@ function capabilityInputNames(folder) {
25183
25411
  if (!properties || typeof properties !== "object" || Array.isArray(properties)) return /* @__PURE__ */ new Set();
25184
25412
  return new Set(Object.keys(properties));
25185
25413
  }
25414
+ function inheritedGenericStepInput(capability, parentArgs, cwd) {
25415
+ const input = workflowInputContext(parentArgs);
25416
+ const folder = resolveCapabilityFolder(capability, hydratedCapabilitiesRoot(cwd));
25417
+ if (!folder) return input;
25418
+ if (!folder.contractPath) return input;
25419
+ if (folder.config.inputSchema?.additionalProperties !== false) return input;
25420
+ const accepted = capabilityInputNames(folder);
25421
+ if (accepted.size === 0) return input;
25422
+ const routing = /* @__PURE__ */ new Set(["base"]);
25423
+ return Object.fromEntries(Object.entries(input).filter(([name]) => accepted.has(name) || routing.has(name)));
25424
+ }
25186
25425
  function cloneWorkflowSteps(steps) {
25187
25426
  return Object.fromEntries(Object.entries(steps).map(([id, step]) => [id, { ...step }]));
25188
25427
  }
@@ -25454,6 +25693,7 @@ var init_job = __esm({
25454
25693
  init_workflowDefinitions();
25455
25694
  init_workflowRunLease();
25456
25695
  init_workflowRunState();
25696
+ init_workflowStepApproval();
25457
25697
  init_workflowValidation();
25458
25698
  init_jobIdentity();
25459
25699
  init_jobIdentity();
@@ -29781,7 +30021,7 @@ import { createInterface as createInterface2 } from "readline";
29781
30021
 
29782
30022
  // src/terminal/brain-terminal-adapters.ts
29783
30023
  import { spawn as spawn9 } from "child_process";
29784
- import { createHash as createHash9, randomBytes as randomBytes2 } from "crypto";
30024
+ import { createHash as createHash10, randomBytes as randomBytes2 } from "crypto";
29785
30025
  import { mkdir, readFile, rename, writeFile as writeFile2 } from "fs/promises";
29786
30026
  import * as path57 from "path";
29787
30027
  function runTerminalCommand(command, args, input) {
@@ -29804,7 +30044,7 @@ function runTerminalCommand(command, args, input) {
29804
30044
  });
29805
30045
  }
29806
30046
  function storeKey(id) {
29807
- return createHash9("sha256").update(id).digest("hex");
30047
+ return createHash10("sha256").update(id).digest("hex");
29808
30048
  }
29809
30049
  function isStoredSession(value) {
29810
30050
  if (!value || typeof value !== "object") return false;
@@ -29909,7 +30149,7 @@ var TmuxBrainTerminalRuntime = class {
29909
30149
  };
29910
30150
 
29911
30151
  // src/terminal/brain-terminal-session.ts
29912
- import { createHash as createHash10 } from "crypto";
30152
+ import { createHash as createHash11 } from "crypto";
29913
30153
  var MAX_CAPTURE_CHARS = 2e5;
29914
30154
  function requiredIdentifier(value, name, max = 240) {
29915
30155
  if (typeof value !== "string" || !value.trim() || value.length > max) {
@@ -29990,7 +30230,7 @@ function parseBrainTerminalCommand(value) {
29990
30230
  }
29991
30231
  }
29992
30232
  function sessionName(id) {
29993
- return `kody_${createHash10("sha256").update(id).digest("hex").slice(0, 32)}`;
30233
+ return `kody_${createHash11("sha256").update(id).digest("hex").slice(0, 32)}`;
29994
30234
  }
29995
30235
  function stateEvent(session) {
29996
30236
  return {
@@ -600,7 +600,7 @@ export interface Job {
600
600
  }
601
601
 
602
602
  export interface WorkflowRunState {
603
- status: "running" | "blocked" | "failed" | "done"
603
+ status: "running" | "waiting-approval" | "blocked" | "failed" | "done"
604
604
  /** Immutable input supplied when this workflow run started. */
605
605
  input?: Record<string, unknown>
606
606
  /** Hash of the workflow definition used by this run. */
@@ -608,6 +608,14 @@ export interface WorkflowRunState {
608
608
  currentStepId?: string
609
609
  completedStepIds: string[]
610
610
  transitionCounts: Record<string, number>
611
+ approval?: {
612
+ stepId: string
613
+ action: string
614
+ contextHash: string
615
+ status: "pending" | "approved" | "consumed"
616
+ approvedAt?: string
617
+ approvedBy?: string
618
+ }
611
619
  /** Exact per-step handoffs for audit, resume, and debugging. */
612
620
  steps?: Record<
613
621
  string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.643",
3
+ "version": "0.4.645",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative implementation profiles.",
5
5
  "license": "MIT",
6
6
  "repository": {