agent-relay 4.0.2 → 4.0.4
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/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +7906 -2084
- package/dist/packages/sdk/src/provisioner/seeder.d.ts +17 -0
- package/dist/packages/sdk/src/provisioner/seeder.d.ts.map +1 -0
- package/dist/packages/sdk/src/provisioner/seeder.js +419 -0
- package/dist/packages/sdk/src/provisioner/seeder.js.map +1 -0
- package/dist/packages/sdk/src/provisioner/token.d.ts +38 -0
- package/dist/packages/sdk/src/provisioner/token.d.ts.map +1 -0
- package/dist/packages/sdk/src/provisioner/token.js +74 -0
- package/dist/packages/sdk/src/provisioner/token.js.map +1 -0
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +7 -3
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/commands/on/provision.d.ts.map +1 -1
- package/dist/src/cli/commands/on/provision.js +8 -3
- package/dist/src/cli/commands/on/provision.js.map +1 -1
- package/dist/src/cli/commands/on/start.d.ts +5 -0
- package/dist/src/cli/commands/on/start.d.ts.map +1 -1
- package/dist/src/cli/commands/on/start.js +126 -88
- package/dist/src/cli/commands/on/start.js.map +1 -1
- package/dist/src/cli/commands/on/symlink-mount.d.ts +12 -0
- package/dist/src/cli/commands/on/symlink-mount.d.ts.map +1 -0
- package/dist/src/cli/commands/on/symlink-mount.js +304 -0
- package/dist/src/cli/commands/on/symlink-mount.js.map +1 -0
- package/dist/src/cli/commands/on.d.ts.map +1 -1
- package/dist/src/cli/commands/on.js +3 -0
- package/dist/src/cli/commands/on.js.map +1 -1
- package/install.sh +4 -0
- package/package.json +9 -9
- package/packages/acp-bridge/package.json +2 -2
- package/packages/brand/package.json +1 -1
- package/packages/cloud/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/client.d.ts +3 -10
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +2 -0
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/provisioner/__tests__/audit.test.d.ts +2 -0
- package/packages/sdk/dist/provisioner/__tests__/audit.test.d.ts.map +1 -0
- package/packages/sdk/dist/provisioner/__tests__/audit.test.js +45 -0
- package/packages/sdk/dist/provisioner/__tests__/audit.test.js.map +1 -0
- package/packages/sdk/dist/provisioner/__tests__/compiler.test.d.ts +2 -0
- package/packages/sdk/dist/provisioner/__tests__/compiler.test.d.ts.map +1 -0
- package/packages/sdk/dist/provisioner/__tests__/compiler.test.js +345 -0
- package/packages/sdk/dist/provisioner/__tests__/compiler.test.js.map +1 -0
- package/packages/sdk/dist/provisioner/__tests__/presets.test.d.ts +2 -0
- package/packages/sdk/dist/provisioner/__tests__/presets.test.d.ts.map +1 -0
- package/packages/sdk/dist/provisioner/__tests__/presets.test.js +23 -0
- package/packages/sdk/dist/provisioner/__tests__/presets.test.js.map +1 -0
- package/packages/sdk/dist/provisioner/__tests__/seeder.test.d.ts +2 -0
- package/packages/sdk/dist/provisioner/__tests__/seeder.test.d.ts.map +1 -0
- package/packages/sdk/dist/provisioner/__tests__/seeder.test.js +224 -0
- package/packages/sdk/dist/provisioner/__tests__/seeder.test.js.map +1 -0
- package/packages/sdk/dist/provisioner/__tests__/tar-seeder.test.d.ts +2 -0
- package/packages/sdk/dist/provisioner/__tests__/tar-seeder.test.d.ts.map +1 -0
- package/packages/sdk/dist/provisioner/__tests__/tar-seeder.test.js +191 -0
- package/packages/sdk/dist/provisioner/__tests__/tar-seeder.test.js.map +1 -0
- package/packages/sdk/dist/provisioner/__tests__/token-factory.test.d.ts +2 -0
- package/packages/sdk/dist/provisioner/__tests__/token-factory.test.d.ts.map +1 -0
- package/packages/sdk/dist/provisioner/__tests__/token-factory.test.js +127 -0
- package/packages/sdk/dist/provisioner/__tests__/token-factory.test.js.map +1 -0
- package/packages/sdk/dist/provisioner/__tests__/token.test.d.ts +2 -0
- package/packages/sdk/dist/provisioner/__tests__/token.test.d.ts.map +1 -0
- package/packages/sdk/dist/provisioner/__tests__/token.test.js +44 -0
- package/packages/sdk/dist/provisioner/__tests__/token.test.js.map +1 -0
- package/packages/sdk/dist/provisioner/audit.d.ts +19 -0
- package/packages/sdk/dist/provisioner/audit.d.ts.map +1 -0
- package/packages/sdk/dist/provisioner/audit.js +74 -0
- package/packages/sdk/dist/provisioner/audit.js.map +1 -0
- package/packages/sdk/dist/provisioner/compiler.d.ts +23 -0
- package/packages/sdk/dist/provisioner/compiler.d.ts.map +1 -0
- package/packages/sdk/dist/provisioner/compiler.js +355 -0
- package/packages/sdk/dist/provisioner/compiler.js.map +1 -0
- package/packages/sdk/dist/provisioner/index.d.ts +9 -0
- package/packages/sdk/dist/provisioner/index.d.ts.map +1 -0
- package/packages/sdk/dist/provisioner/index.js +266 -0
- package/packages/sdk/dist/provisioner/index.js.map +1 -0
- package/packages/sdk/dist/provisioner/mount.d.ts +14 -0
- package/packages/sdk/dist/provisioner/mount.d.ts.map +1 -0
- package/packages/sdk/dist/provisioner/mount.js +329 -0
- package/packages/sdk/dist/provisioner/mount.js.map +1 -0
- package/packages/sdk/dist/provisioner/seeder.d.ts +17 -0
- package/packages/sdk/dist/provisioner/seeder.d.ts.map +1 -0
- package/packages/sdk/dist/provisioner/seeder.js +419 -0
- package/packages/sdk/dist/provisioner/seeder.js.map +1 -0
- package/packages/sdk/dist/provisioner/token.d.ts +38 -0
- package/packages/sdk/dist/provisioner/token.d.ts.map +1 -0
- package/packages/sdk/dist/provisioner/token.js +74 -0
- package/packages/sdk/dist/provisioner/token.js.map +1 -0
- package/packages/sdk/dist/provisioner/types.d.ts +133 -0
- package/packages/sdk/dist/provisioner/types.d.ts.map +1 -0
- package/packages/sdk/dist/provisioner/types.js +2 -0
- package/packages/sdk/dist/provisioner/types.js.map +1 -0
- package/packages/sdk/dist/relay.d.ts +6 -0
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +17 -5
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/types.d.ts +9 -0
- package/packages/sdk/dist/types.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/__tests__/e2e-permissions.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/e2e-permissions.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/e2e-permissions.test.js +331 -0
- package/packages/sdk/dist/workflows/__tests__/e2e-permissions.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/permission-types.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/permission-types.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/permission-types.test.js +124 -0
- package/packages/sdk/dist/workflows/__tests__/permission-types.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/permissions-integration.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/permissions-integration.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/permissions-integration.test.js +526 -0
- package/packages/sdk/dist/workflows/__tests__/permissions-integration.test.js.map +1 -0
- package/packages/sdk/dist/workflows/dry-run-format.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/dry-run-format.js +8 -0
- package/packages/sdk/dist/workflows/dry-run-format.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +14 -0
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +455 -6
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/types.d.ts +190 -0
- package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/types.js +29 -0
- package/packages/sdk/dist/workflows/types.js.map +1 -1
- package/packages/sdk/package.json +6 -2
- package/packages/sdk/src/__tests__/orchestration-upgrades.test.ts +123 -1
- package/packages/sdk/src/__tests__/provisioner-mount.test.ts +126 -0
- package/packages/sdk/src/__tests__/spawn-token.test.ts +41 -0
- package/packages/sdk/src/__tests__/workflow-runner.test.ts +77 -45
- package/packages/sdk/src/client.ts +4 -8
- package/packages/sdk/src/provisioner/__tests__/audit.test.ts +62 -0
- package/packages/sdk/src/provisioner/__tests__/compiler.test.ts +369 -0
- package/packages/sdk/src/provisioner/__tests__/presets.test.ts +25 -0
- package/packages/sdk/src/provisioner/__tests__/seeder.test.ts +284 -0
- package/packages/sdk/src/provisioner/__tests__/tar-seeder.test.ts +249 -0
- package/packages/sdk/src/provisioner/__tests__/token-factory.test.ts +172 -0
- package/packages/sdk/src/provisioner/__tests__/token.test.ts +53 -0
- package/packages/sdk/src/provisioner/audit.ts +104 -0
- package/packages/sdk/src/provisioner/compiler.ts +498 -0
- package/packages/sdk/src/provisioner/index.ts +332 -0
- package/packages/sdk/src/provisioner/mount.ts +419 -0
- package/packages/sdk/src/provisioner/seeder.ts +571 -0
- package/packages/sdk/src/provisioner/token.ts +112 -0
- package/packages/sdk/src/provisioner/types.ts +188 -0
- package/packages/sdk/src/relay.ts +31 -9
- package/packages/sdk/src/types.ts +9 -0
- package/packages/sdk/src/workflows/__tests__/e2e-permissions.test.ts +407 -0
- package/packages/sdk/src/workflows/__tests__/fixtures/.agentignore +2 -0
- package/packages/sdk/src/workflows/__tests__/fixtures/.reader.agentreadonly +2 -0
- package/packages/sdk/src/workflows/__tests__/fixtures/permission-test.yaml +42 -0
- package/packages/sdk/src/workflows/__tests__/permission-types.test.ts +154 -0
- package/packages/sdk/src/workflows/__tests__/permissions-integration.test.ts +649 -0
- package/packages/sdk/src/workflows/builtin-templates/bug-fix.yaml +13 -9
- package/packages/sdk/src/workflows/builtin-templates/code-review.yaml +12 -8
- package/packages/sdk/src/workflows/builtin-templates/competitive.yaml +11 -7
- package/packages/sdk/src/workflows/builtin-templates/documentation.yaml +16 -8
- package/packages/sdk/src/workflows/builtin-templates/feature-dev.yaml +13 -9
- package/packages/sdk/src/workflows/builtin-templates/refactor.yaml +13 -9
- package/packages/sdk/src/workflows/builtin-templates/review-loop.yaml +14 -10
- package/packages/sdk/src/workflows/builtin-templates/security-audit.yaml +19 -9
- package/packages/sdk/src/workflows/dry-run-format.ts +14 -1
- package/packages/sdk/src/workflows/runner.ts +559 -6
- package/packages/sdk/src/workflows/schema.json +204 -114
- package/packages/sdk/src/workflows/types.ts +266 -1
- package/packages/sdk/vitest.config.ts +5 -1
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
|
@@ -22,6 +22,7 @@ import { readFile, writeFile, mkdir, unlink } from 'node:fs/promises';
|
|
|
22
22
|
import { tmpdir } from 'node:os';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import chalk from 'chalk';
|
|
25
|
+
import ignore from 'ignore';
|
|
25
26
|
|
|
26
27
|
import { parse as parseYaml } from 'yaml';
|
|
27
28
|
import { stripAnsi as stripAnsiFn } from '../pty.js';
|
|
@@ -37,6 +38,8 @@ import {
|
|
|
37
38
|
CustomStepsParseError,
|
|
38
39
|
CustomStepResolutionError,
|
|
39
40
|
} from './custom-steps.js';
|
|
41
|
+
import { provisionWorkflowAgents, resolveAgentPermissions } from '../provisioner/index.js';
|
|
42
|
+
import type { MountHandle } from '../provisioner/mount.js';
|
|
40
43
|
import { collectCliSession, type CliSessionReport } from './cli-session-collector.js';
|
|
41
44
|
import { executeApiStep } from './api-executor.js';
|
|
42
45
|
import { ChannelMessenger } from './channel-messenger.js';
|
|
@@ -55,8 +58,10 @@ import {
|
|
|
55
58
|
type VariableContext,
|
|
56
59
|
} from './template-resolver.js';
|
|
57
60
|
import type {
|
|
61
|
+
AccessPreset,
|
|
58
62
|
AgentCli,
|
|
59
63
|
AgentDefinition,
|
|
64
|
+
AgentPermissions,
|
|
60
65
|
AgentPreset,
|
|
61
66
|
CompletionEvidenceChannelOrigin,
|
|
62
67
|
CompletionEvidenceChannelPost,
|
|
@@ -69,6 +74,7 @@ import type {
|
|
|
69
74
|
ErrorHandlingConfig,
|
|
70
75
|
IdleNudgeConfig,
|
|
71
76
|
PathDefinition,
|
|
77
|
+
PermissionProfileDefinition,
|
|
72
78
|
PreflightCheck,
|
|
73
79
|
RelayYamlConfig,
|
|
74
80
|
StepCompletionDecision,
|
|
@@ -391,6 +397,10 @@ export class WorkflowRunner {
|
|
|
391
397
|
private currentRunId?: string;
|
|
392
398
|
/** Live Agent handles keyed by name, for hub-mediated nudging. */
|
|
393
399
|
private readonly activeAgentHandles = new Map<string, Agent>();
|
|
400
|
+
/** Per-agent workflow tokens for relay/relayfile auth across spawn modes. */
|
|
401
|
+
private readonly agentTokens = new Map<string, string>();
|
|
402
|
+
/** Per-agent relayfile mounts keyed by logical agent definition name. */
|
|
403
|
+
private readonly agentMounts = new Map<string, MountHandle>();
|
|
394
404
|
|
|
395
405
|
// PTY-based output capture: accumulate terminal output per-agent
|
|
396
406
|
private readonly ptyOutputBuffers = new Map<string, string[]>();
|
|
@@ -492,6 +502,355 @@ export class WorkflowRunner {
|
|
|
492
502
|
return { resolved, errors, warnings };
|
|
493
503
|
}
|
|
494
504
|
|
|
505
|
+
private validatePermissions(
|
|
506
|
+
agents: AgentDefinition[] | undefined,
|
|
507
|
+
permissionProfiles: RelayYamlConfig['permission_profiles'],
|
|
508
|
+
source = '<config>'
|
|
509
|
+
): { errors: string[]; warnings: string[] } {
|
|
510
|
+
const errors: string[] = [];
|
|
511
|
+
const warnings: string[] = [];
|
|
512
|
+
const accessPresets = new Set<AccessPreset>(['readonly', 'readwrite', 'restricted', 'full']);
|
|
513
|
+
const profiles = permissionProfiles ?? {};
|
|
514
|
+
const profileNames = new Set(Object.keys(profiles));
|
|
515
|
+
|
|
516
|
+
const validateStringArray = (value: unknown, label: string): string[] | undefined => {
|
|
517
|
+
if (value === undefined) {
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
if (!Array.isArray(value)) {
|
|
521
|
+
errors.push(`${label} must be an array of strings`);
|
|
522
|
+
return undefined;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const normalized: string[] = [];
|
|
526
|
+
for (const entry of value) {
|
|
527
|
+
if (typeof entry !== 'string') {
|
|
528
|
+
errors.push(`${label} must be an array of strings`);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
normalized.push(entry);
|
|
532
|
+
}
|
|
533
|
+
return normalized;
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
const validateGlobPattern = (pattern: string, label: string): void => {
|
|
537
|
+
const trimmed = pattern.trim();
|
|
538
|
+
if (trimmed === '') {
|
|
539
|
+
errors.push(`${label} must not contain empty glob patterns`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (trimmed.includes('\0')) {
|
|
543
|
+
errors.push(`${label} contains an invalid glob pattern "${pattern}" (NUL byte)`);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
let escaped = false;
|
|
548
|
+
let bracketDepth = 0;
|
|
549
|
+
let braceDepth = 0;
|
|
550
|
+
|
|
551
|
+
for (const ch of trimmed) {
|
|
552
|
+
if (escaped) {
|
|
553
|
+
escaped = false;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
if (ch === '\\') {
|
|
557
|
+
escaped = true;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (ch === '[') {
|
|
561
|
+
bracketDepth += 1;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (ch === ']' && bracketDepth > 0) {
|
|
565
|
+
bracketDepth -= 1;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (ch === '{') {
|
|
569
|
+
braceDepth += 1;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (ch === '}' && braceDepth > 0) {
|
|
573
|
+
braceDepth -= 1;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (escaped) {
|
|
578
|
+
errors.push(`${label} contains an invalid glob pattern "${pattern}" (dangling escape)`);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (bracketDepth > 0) {
|
|
582
|
+
errors.push(`${label} contains an invalid glob pattern "${pattern}" (unclosed character class)`);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (braceDepth > 0) {
|
|
586
|
+
errors.push(`${label} contains an invalid glob pattern "${pattern}" (unclosed brace expansion)`);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
ignore().add([trimmed]);
|
|
592
|
+
} catch (err) {
|
|
593
|
+
errors.push(
|
|
594
|
+
`${label} contains an invalid glob pattern "${pattern}" (${err instanceof Error ? err.message : String(err)})`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const validatePermissionObject = (
|
|
600
|
+
permissions: unknown,
|
|
601
|
+
label: string,
|
|
602
|
+
options: { allowProfileReference: boolean }
|
|
603
|
+
): void => {
|
|
604
|
+
if (typeof permissions === 'string') {
|
|
605
|
+
const shorthand = permissions.trim();
|
|
606
|
+
if (shorthand === '') {
|
|
607
|
+
errors.push(`${label} must not be empty`);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (accessPresets.has(shorthand as AccessPreset)) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (options.allowProfileReference) {
|
|
616
|
+
if (!profileNames.has(shorthand)) {
|
|
617
|
+
errors.push(`${label} references unknown permission profile "${shorthand}"`);
|
|
618
|
+
}
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
errors.push(`${label} must be an object when provided`);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (typeof permissions !== 'object' || permissions === null) {
|
|
627
|
+
errors.push(`${label} must be an object when provided`);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const permissionRecord = permissions as Record<string, unknown>;
|
|
632
|
+
|
|
633
|
+
if (permissionRecord.description !== undefined && typeof permissionRecord.description !== 'string') {
|
|
634
|
+
errors.push(`${label}.description must be a string when provided`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (permissionRecord.profile !== undefined) {
|
|
638
|
+
if (!options.allowProfileReference) {
|
|
639
|
+
errors.push(`${label}.profile is only supported on agent permissions`);
|
|
640
|
+
} else if (typeof permissionRecord.profile !== 'string') {
|
|
641
|
+
errors.push(`${label}.profile must be a string when provided`);
|
|
642
|
+
} else if (permissionRecord.profile.trim() === '') {
|
|
643
|
+
errors.push(`${label}.profile must not be empty`);
|
|
644
|
+
} else if (!profileNames.has(permissionRecord.profile)) {
|
|
645
|
+
errors.push(`${label}.profile references unknown permission profile "${permissionRecord.profile}"`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (permissionRecord.why !== undefined && typeof permissionRecord.why !== 'string') {
|
|
650
|
+
errors.push(`${label}.why must be a string when provided`);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (
|
|
654
|
+
permissionRecord.access !== undefined &&
|
|
655
|
+
!accessPresets.has(permissionRecord.access as AccessPreset)
|
|
656
|
+
) {
|
|
657
|
+
errors.push(`${label}.access must be one of readonly, readwrite, restricted, full`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (permissionRecord.inherit !== undefined && typeof permissionRecord.inherit !== 'boolean') {
|
|
661
|
+
errors.push(`${label}.inherit must be a boolean when provided`);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (permissionRecord.network !== undefined) {
|
|
665
|
+
if (typeof permissionRecord.network === 'boolean') {
|
|
666
|
+
// valid: boolean form
|
|
667
|
+
} else if (
|
|
668
|
+
typeof permissionRecord.network === 'object' &&
|
|
669
|
+
permissionRecord.network !== null &&
|
|
670
|
+
!Array.isArray(permissionRecord.network)
|
|
671
|
+
) {
|
|
672
|
+
const net = permissionRecord.network as Record<string, unknown>;
|
|
673
|
+
validateStringArray(net.allow, `${label}.network.allow`);
|
|
674
|
+
validateStringArray(net.deny, `${label}.network.deny`);
|
|
675
|
+
} else {
|
|
676
|
+
errors.push(`${label}.network must be a boolean or an object with allow/deny arrays`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (permissionRecord.files !== undefined) {
|
|
681
|
+
if (
|
|
682
|
+
typeof permissionRecord.files !== 'object' ||
|
|
683
|
+
permissionRecord.files === null ||
|
|
684
|
+
Array.isArray(permissionRecord.files)
|
|
685
|
+
) {
|
|
686
|
+
errors.push(`${label}.files must be an object when provided`);
|
|
687
|
+
} else {
|
|
688
|
+
const files = permissionRecord.files as Record<string, unknown>;
|
|
689
|
+
const read = validateStringArray(files.read, `${label}.files.read`);
|
|
690
|
+
const write = validateStringArray(files.write, `${label}.files.write`);
|
|
691
|
+
const deny = validateStringArray(files.deny, `${label}.files.deny`);
|
|
692
|
+
|
|
693
|
+
for (const pattern of read ?? []) {
|
|
694
|
+
validateGlobPattern(pattern, `${label}.files.read`);
|
|
695
|
+
}
|
|
696
|
+
for (const pattern of write ?? []) {
|
|
697
|
+
validateGlobPattern(pattern, `${label}.files.write`);
|
|
698
|
+
}
|
|
699
|
+
for (const pattern of deny ?? []) {
|
|
700
|
+
validateGlobPattern(pattern, `${label}.files.deny`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (permissionRecord.access === 'readonly' && (write?.length ?? 0) > 0) {
|
|
704
|
+
warnings.push(`${label} sets access to "readonly" but also defines files.write entries`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const scopes = validateStringArray(permissionRecord.scopes, `${label}.scopes`);
|
|
710
|
+
for (const scope of scopes ?? []) {
|
|
711
|
+
if (scope.trim() === '') {
|
|
712
|
+
errors.push(`${label}.scopes must not contain empty strings`);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if (!/^[^:\s]+:[^:\s]+:[^:\s]+:.+$/u.test(scope)) {
|
|
716
|
+
errors.push(`${label}.scopes entry "${scope}" must follow plane:resource:action:path format`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const exec = validateStringArray(permissionRecord.exec, `${label}.exec`);
|
|
721
|
+
for (const entry of exec ?? []) {
|
|
722
|
+
if (entry.trim() === '') {
|
|
723
|
+
errors.push(`${label}.exec must not contain empty strings`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
if (permissionProfiles !== undefined) {
|
|
729
|
+
if (
|
|
730
|
+
typeof permissionProfiles !== 'object' ||
|
|
731
|
+
permissionProfiles === null ||
|
|
732
|
+
Array.isArray(permissionProfiles)
|
|
733
|
+
) {
|
|
734
|
+
errors.push(`${source}: permission_profiles must be an object when provided`);
|
|
735
|
+
} else {
|
|
736
|
+
for (const [profileName, profile] of Object.entries(permissionProfiles)) {
|
|
737
|
+
if (profileName.trim() === '') {
|
|
738
|
+
errors.push(`${source}: permission_profiles keys must not be empty`);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
validatePermissionObject(profile, `${source}: permission_profiles.${profileName}`, {
|
|
742
|
+
allowProfileReference: false,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (!agents || agents.length === 0) {
|
|
749
|
+
return { errors, warnings };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
for (const agent of agents) {
|
|
753
|
+
if (agent.permissions === undefined) {
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
validatePermissionObject(agent.permissions, `${source}: agent "${agent.name}" permissions`, {
|
|
757
|
+
allowProfileReference: true,
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return { errors, warnings };
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private mergePermissionLists(
|
|
765
|
+
base: readonly string[] | undefined,
|
|
766
|
+
override: readonly string[] | undefined
|
|
767
|
+
): string[] | undefined {
|
|
768
|
+
const merged = [
|
|
769
|
+
...new Set([...(base ?? []), ...(override ?? [])].map((value) => value.trim()).filter(Boolean)),
|
|
770
|
+
];
|
|
771
|
+
return merged.length > 0 ? merged : undefined;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
private mergePermissionFiles(
|
|
775
|
+
base: AgentPermissions['files'],
|
|
776
|
+
override: AgentPermissions['files']
|
|
777
|
+
): AgentPermissions['files'] {
|
|
778
|
+
const merged = {
|
|
779
|
+
read: this.mergePermissionLists(base?.read, override?.read),
|
|
780
|
+
write: this.mergePermissionLists(base?.write, override?.write),
|
|
781
|
+
deny: this.mergePermissionLists(base?.deny, override?.deny),
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
return merged.read || merged.write || merged.deny ? merged : undefined;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private mergePermissionProfile(
|
|
788
|
+
profile: PermissionProfileDefinition,
|
|
789
|
+
permissions: AgentPermissions
|
|
790
|
+
): AgentPermissions {
|
|
791
|
+
const merged: AgentPermissions = {
|
|
792
|
+
description: permissions.description ?? profile.description,
|
|
793
|
+
profile: permissions.profile,
|
|
794
|
+
why: permissions.why ?? profile.why,
|
|
795
|
+
access: permissions.access ?? profile.access,
|
|
796
|
+
inherit: permissions.inherit ?? profile.inherit,
|
|
797
|
+
files: this.mergePermissionFiles(profile.files, permissions.files),
|
|
798
|
+
scopes: this.mergePermissionLists(profile.scopes, permissions.scopes),
|
|
799
|
+
network: permissions.network ?? profile.network,
|
|
800
|
+
exec: this.mergePermissionLists(profile.exec, permissions.exec),
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
return Object.fromEntries(
|
|
804
|
+
Object.entries(merged).filter(([, value]) => value !== undefined)
|
|
805
|
+
) as AgentPermissions;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private applyPermissionProfiles(config: RelayYamlConfig): RelayYamlConfig {
|
|
809
|
+
if (!config.permission_profiles || Object.keys(config.permission_profiles).length === 0) {
|
|
810
|
+
return config;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return {
|
|
814
|
+
...config,
|
|
815
|
+
agents: config.agents.map((agent) => {
|
|
816
|
+
const rawPermissions = agent.permissions;
|
|
817
|
+
if (!rawPermissions) {
|
|
818
|
+
return agent;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const normalizedPermissions =
|
|
822
|
+
typeof rawPermissions === 'string'
|
|
823
|
+
? ({
|
|
824
|
+
...(config.permission_profiles?.[rawPermissions]
|
|
825
|
+
? { profile: rawPermissions }
|
|
826
|
+
: { access: rawPermissions as AccessPreset }),
|
|
827
|
+
} as AgentPermissions)
|
|
828
|
+
: rawPermissions;
|
|
829
|
+
|
|
830
|
+
const profileName = normalizedPermissions.profile;
|
|
831
|
+
if (!profileName) {
|
|
832
|
+
return {
|
|
833
|
+
...agent,
|
|
834
|
+
permissions: normalizedPermissions,
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const profile = config.permission_profiles?.[profileName];
|
|
839
|
+
if (!profile) {
|
|
840
|
+
return {
|
|
841
|
+
...agent,
|
|
842
|
+
permissions: normalizedPermissions,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return {
|
|
847
|
+
...agent,
|
|
848
|
+
permissions: this.mergePermissionProfile(profile, normalizedPermissions),
|
|
849
|
+
};
|
|
850
|
+
}),
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
495
854
|
/**
|
|
496
855
|
* Resolve an agent's effective working directory, considering `workdir` (named path reference)
|
|
497
856
|
* and `cwd` (explicit path). `workdir` takes precedence when both are set.
|
|
@@ -534,6 +893,36 @@ export class WorkflowRunner {
|
|
|
534
893
|
return this.resolveStepWorkdir(step) ?? (agentDef ? this.resolveAgentCwd(agentDef) : this.cwd);
|
|
535
894
|
}
|
|
536
895
|
|
|
896
|
+
private resolveMountedCwd(agentName: string, cwd: string): string {
|
|
897
|
+
const mount = this.agentMounts.get(agentName);
|
|
898
|
+
if (!mount) {
|
|
899
|
+
return cwd;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const relative = path.relative(this.cwd, cwd);
|
|
903
|
+
if (relative === '') {
|
|
904
|
+
return mount.mountPoint;
|
|
905
|
+
}
|
|
906
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`)) {
|
|
907
|
+
return cwd;
|
|
908
|
+
}
|
|
909
|
+
return path.resolve(mount.mountPoint, relative);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
private resolveExecutionCwd(step: WorkflowStep, agentDef?: AgentDefinition): string {
|
|
913
|
+
const cwd = this.resolveEffectiveCwd(step, agentDef);
|
|
914
|
+
if (!agentDef) {
|
|
915
|
+
return cwd;
|
|
916
|
+
}
|
|
917
|
+
return this.resolveMountedCwd(agentDef.name, cwd);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private async stopProvisionedMounts(): Promise<void> {
|
|
921
|
+
const handles = [...this.agentMounts.values()];
|
|
922
|
+
this.agentMounts.clear();
|
|
923
|
+
await Promise.all(handles.map((handle) => handle.stop().catch(() => undefined)));
|
|
924
|
+
}
|
|
925
|
+
|
|
537
926
|
private static readonly EVIDENCE_IGNORED_DIRS = new Set([
|
|
538
927
|
'.git',
|
|
539
928
|
'.agent-relay',
|
|
@@ -1138,6 +1527,48 @@ export class WorkflowRunner {
|
|
|
1138
1527
|
};
|
|
1139
1528
|
}
|
|
1140
1529
|
|
|
1530
|
+
private async provisionAgents(config: RelayYamlConfig): Promise<void> {
|
|
1531
|
+
this.agentTokens.clear();
|
|
1532
|
+
await this.stopProvisionedMounts();
|
|
1533
|
+
|
|
1534
|
+
const agentsToProvision: Record<string, NonNullable<AgentDefinition['permissions']>> = {};
|
|
1535
|
+
for (const agent of config.agents) {
|
|
1536
|
+
if (agent.permissions) {
|
|
1537
|
+
agentsToProvision[agent.name] = agent.permissions;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
const agentNames = Object.keys(agentsToProvision);
|
|
1542
|
+
if (agentNames.length === 0) {
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const relayEnv = {
|
|
1547
|
+
...process.env,
|
|
1548
|
+
...(this.getRelayEnv() ?? {}),
|
|
1549
|
+
};
|
|
1550
|
+
const result = await provisionWorkflowAgents({
|
|
1551
|
+
secret:
|
|
1552
|
+
this.envSecrets?.RELAY_AUTH_SECRET ?? relayEnv.RELAY_AUTH_SECRET ?? randomBytes(32).toString('hex'),
|
|
1553
|
+
workspace: this.workspaceId,
|
|
1554
|
+
projectDir: this.cwd,
|
|
1555
|
+
relayfileBaseUrl: relayEnv.RELAYFILE_BASE_URL ?? 'http://127.0.0.1:8080',
|
|
1556
|
+
agents: agentsToProvision,
|
|
1557
|
+
tokenTtlSeconds: 3600,
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
for (const [agentName, token] of result.tokens) {
|
|
1561
|
+
this.agentTokens.set(agentName, token);
|
|
1562
|
+
}
|
|
1563
|
+
for (const [agentName, mount] of result.mounts) {
|
|
1564
|
+
this.agentMounts.set(agentName, mount);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
this.log(
|
|
1568
|
+
`Provisioned workflow tokens for ${result.tokens.size} agent${result.tokens.size === 1 ? '' : 's'}`
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1141
1572
|
private getRelaycastBaseUrl(): string {
|
|
1142
1573
|
return (
|
|
1143
1574
|
this.relayOptions.env?.RELAYCAST_BASE_URL ??
|
|
@@ -1248,11 +1679,34 @@ export class WorkflowRunner {
|
|
|
1248
1679
|
parseYamlString(raw: string, source = '<string>'): RelayYamlConfig {
|
|
1249
1680
|
const parsed = parseYaml(raw);
|
|
1250
1681
|
this.validateConfig(parsed, source);
|
|
1251
|
-
const config = parsed as RelayYamlConfig;
|
|
1682
|
+
const config = this.normalizeLegacyPermissionConfig(parsed as RelayYamlConfig);
|
|
1252
1683
|
config.agents ??= [];
|
|
1253
1684
|
return config;
|
|
1254
1685
|
}
|
|
1255
1686
|
|
|
1687
|
+
private normalizeLegacyPermissionConfig(config: RelayYamlConfig): RelayYamlConfig {
|
|
1688
|
+
const legacyPermissions = (
|
|
1689
|
+
config as RelayYamlConfig & {
|
|
1690
|
+
permissions?: { profiles?: RelayYamlConfig['permission_profiles'] };
|
|
1691
|
+
}
|
|
1692
|
+
).permissions;
|
|
1693
|
+
|
|
1694
|
+
if (
|
|
1695
|
+
config.permission_profiles === undefined &&
|
|
1696
|
+
legacyPermissions &&
|
|
1697
|
+
typeof legacyPermissions === 'object' &&
|
|
1698
|
+
legacyPermissions.profiles &&
|
|
1699
|
+
typeof legacyPermissions.profiles === 'object'
|
|
1700
|
+
) {
|
|
1701
|
+
return {
|
|
1702
|
+
...config,
|
|
1703
|
+
permission_profiles: legacyPermissions.profiles,
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
return config;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1256
1710
|
/** Validate a config object against the RelayYamlConfig shape. */
|
|
1257
1711
|
validateConfig(config: unknown, source = '<config>'): asserts config is RelayYamlConfig {
|
|
1258
1712
|
if (typeof config !== 'object' || config === null) {
|
|
@@ -1277,6 +1731,37 @@ export class WorkflowRunner {
|
|
|
1277
1731
|
if (c.agents !== undefined && !Array.isArray(c.agents)) {
|
|
1278
1732
|
throw new Error(`${source}: "agents" must be an array when provided`);
|
|
1279
1733
|
}
|
|
1734
|
+
const legacyPermissions = c.permissions;
|
|
1735
|
+
if (
|
|
1736
|
+
legacyPermissions !== undefined &&
|
|
1737
|
+
(typeof legacyPermissions !== 'object' ||
|
|
1738
|
+
legacyPermissions === null ||
|
|
1739
|
+
Array.isArray(legacyPermissions))
|
|
1740
|
+
) {
|
|
1741
|
+
throw new Error(`${source}: "permissions" must be an object when provided`);
|
|
1742
|
+
}
|
|
1743
|
+
if (
|
|
1744
|
+
c.permission_profiles !== undefined &&
|
|
1745
|
+
(typeof c.permission_profiles !== 'object' ||
|
|
1746
|
+
c.permission_profiles === null ||
|
|
1747
|
+
Array.isArray(c.permission_profiles))
|
|
1748
|
+
) {
|
|
1749
|
+
throw new Error(`${source}: "permission_profiles" must be an object when provided`);
|
|
1750
|
+
}
|
|
1751
|
+
if (
|
|
1752
|
+
c.permission_profiles === undefined &&
|
|
1753
|
+
legacyPermissions !== undefined &&
|
|
1754
|
+
typeof legacyPermissions === 'object' &&
|
|
1755
|
+
legacyPermissions !== null
|
|
1756
|
+
) {
|
|
1757
|
+
const profiles = (legacyPermissions as Record<string, unknown>).profiles;
|
|
1758
|
+
if (
|
|
1759
|
+
profiles !== undefined &&
|
|
1760
|
+
(typeof profiles !== 'object' || profiles === null || Array.isArray(profiles))
|
|
1761
|
+
) {
|
|
1762
|
+
throw new Error(`${source}: "permissions.profiles" must be an object when provided`);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1280
1765
|
|
|
1281
1766
|
for (const agent of c.agents ?? []) {
|
|
1282
1767
|
if (typeof agent !== 'object' || agent === null) {
|
|
@@ -1316,6 +1801,7 @@ export class WorkflowRunner {
|
|
|
1316
1801
|
try {
|
|
1317
1802
|
this.validateConfig(config);
|
|
1318
1803
|
resolved = vars ? this.resolveVariables(config, vars) : config;
|
|
1804
|
+
resolved = this.applyPermissionProfiles(resolved);
|
|
1319
1805
|
} catch (err) {
|
|
1320
1806
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
1321
1807
|
return {
|
|
@@ -1331,7 +1817,11 @@ export class WorkflowRunner {
|
|
|
1331
1817
|
};
|
|
1332
1818
|
}
|
|
1333
1819
|
|
|
1334
|
-
// 1b.
|
|
1820
|
+
// 1b. Validate permissions and resolve named paths
|
|
1821
|
+
const permissionResult = this.validatePermissions(resolved.agents, resolved.permission_profiles);
|
|
1822
|
+
errors.push(...permissionResult.errors);
|
|
1823
|
+
warnings.push(...permissionResult.warnings);
|
|
1824
|
+
|
|
1335
1825
|
const pathResult = this.resolvePathDefinitions(resolved.paths, this.cwd);
|
|
1336
1826
|
errors.push(...pathResult.errors);
|
|
1337
1827
|
warnings.push(...pathResult.warnings);
|
|
@@ -1520,6 +2010,29 @@ export class WorkflowRunner {
|
|
|
1520
2010
|
}
|
|
1521
2011
|
}
|
|
1522
2012
|
|
|
2013
|
+
const permissions = resolved.agents.map((agent) => {
|
|
2014
|
+
const compiled = resolveAgentPermissions(agent.name, agent.permissions, this.cwd, this.workspaceId);
|
|
2015
|
+
const source: NonNullable<DryRunReport['permissions']>[number]['source'] = compiled.sources.some(
|
|
2016
|
+
(entry) => entry.type === 'yaml'
|
|
2017
|
+
)
|
|
2018
|
+
? 'yaml'
|
|
2019
|
+
: compiled.sources.some((entry) => entry.type === 'preset')
|
|
2020
|
+
? 'preset'
|
|
2021
|
+
: compiled.sources.some((entry) => entry.type === 'dotfile')
|
|
2022
|
+
? 'dotfiles'
|
|
2023
|
+
: 'none';
|
|
2024
|
+
|
|
2025
|
+
return {
|
|
2026
|
+
agent: agent.name,
|
|
2027
|
+
access: compiled.effectiveAccess,
|
|
2028
|
+
readPaths: compiled.summary.readonly,
|
|
2029
|
+
writePaths: compiled.summary.readwrite,
|
|
2030
|
+
denyPaths: compiled.summary.denied,
|
|
2031
|
+
scopes: compiled.scopes.length,
|
|
2032
|
+
source,
|
|
2033
|
+
};
|
|
2034
|
+
});
|
|
2035
|
+
|
|
1523
2036
|
// 4. Build agent summary
|
|
1524
2037
|
const agents = resolved.agents.map((a) => ({
|
|
1525
2038
|
name: a.name,
|
|
@@ -1590,6 +2103,7 @@ export class WorkflowRunner {
|
|
|
1590
2103
|
description: workflow.description ?? resolved.description,
|
|
1591
2104
|
pattern: resolved.swarm.pattern,
|
|
1592
2105
|
agents,
|
|
2106
|
+
permissions,
|
|
1593
2107
|
waves,
|
|
1594
2108
|
totalSteps: workflow.steps.length,
|
|
1595
2109
|
maxConcurrency,
|
|
@@ -1900,11 +2414,19 @@ export class WorkflowRunner {
|
|
|
1900
2414
|
this.abortController = new AbortController();
|
|
1901
2415
|
this.paused = false;
|
|
1902
2416
|
|
|
1903
|
-
const resolved = vars ? this.resolveVariables(config, vars) : config;
|
|
2417
|
+
const resolved = this.applyPermissionProfiles(vars ? this.resolveVariables(config, vars) : config);
|
|
1904
2418
|
|
|
1905
2419
|
// Validate config (catches cycles, missing deps, invalid steps, etc.)
|
|
1906
2420
|
this.validateConfig(resolved);
|
|
1907
2421
|
|
|
2422
|
+
const permissionResult = this.validatePermissions(resolved.agents, resolved.permission_profiles);
|
|
2423
|
+
if (permissionResult.errors.length > 0) {
|
|
2424
|
+
throw new Error(`Permission validation failed:\n ${permissionResult.errors.join('\n ')}`);
|
|
2425
|
+
}
|
|
2426
|
+
for (const warning of permissionResult.warnings) {
|
|
2427
|
+
console.warn(`[WorkflowRunner] Warning: ${warning}`);
|
|
2428
|
+
}
|
|
2429
|
+
|
|
1908
2430
|
// Resolve and validate named paths from the top-level `paths` config
|
|
1909
2431
|
const pathResult = this.resolvePathDefinitions(resolved.paths, this.cwd);
|
|
1910
2432
|
if (pathResult.errors.length > 0) {
|
|
@@ -2403,6 +2925,8 @@ export class WorkflowRunner {
|
|
|
2403
2925
|
await this.runPreflightChecks(workflow.preflight, runId);
|
|
2404
2926
|
}
|
|
2405
2927
|
|
|
2928
|
+
await this.provisionAgents(config);
|
|
2929
|
+
|
|
2406
2930
|
this.log(`Executing ${workflow.steps.length} steps (pattern: ${config.swarm.pattern})`);
|
|
2407
2931
|
await this.executeSteps(workflow, stepStates, agentMap, config.errorHandling, runId);
|
|
2408
2932
|
|
|
@@ -2523,6 +3047,8 @@ export class WorkflowRunner {
|
|
|
2523
3047
|
this.currentConfig = undefined;
|
|
2524
3048
|
this.currentRunId = undefined;
|
|
2525
3049
|
this.activeAgentHandles.clear();
|
|
3050
|
+
await this.stopProvisionedMounts();
|
|
3051
|
+
this.agentTokens.clear(); // Prevent workflow-scoped tokens from leaking into a later run.
|
|
2526
3052
|
}
|
|
2527
3053
|
|
|
2528
3054
|
const finalRun = await this.db.getRun(runId);
|
|
@@ -4977,6 +5503,25 @@ export class WorkflowRunner {
|
|
|
4977
5503
|
|
|
4978
5504
|
const stdoutChunks: string[] = [];
|
|
4979
5505
|
const stderrChunks: string[] = [];
|
|
5506
|
+
const env = { ...(this.getRelayEnv() ?? filteredEnv()) };
|
|
5507
|
+
const agentToken = this.agentTokens.get(agentDef.name);
|
|
5508
|
+
const mount = this.agentMounts.get(agentDef.name);
|
|
5509
|
+
if (agentToken) {
|
|
5510
|
+
env.RELAY_AGENT_TOKEN = agentToken;
|
|
5511
|
+
env.RELAYFILE_TOKEN = agentToken;
|
|
5512
|
+
}
|
|
5513
|
+
if (mount) {
|
|
5514
|
+
env.RELAY_WORKSPACE = mount.mountPoint;
|
|
5515
|
+
env.RELAY_AGENT_NAME = agentName;
|
|
5516
|
+
env.RELAYFILE_WORKSPACE = this.workspaceId;
|
|
5517
|
+
env.RELAY_WORKSPACE_ID = this.workspaceId;
|
|
5518
|
+
env.RELAY_DEFAULT_WORKSPACE = this.workspaceId;
|
|
5519
|
+
}
|
|
5520
|
+
env.RELAYFILE_BASE_URL =
|
|
5521
|
+
env.RELAYFILE_BASE_URL ??
|
|
5522
|
+
this.getRelayEnv()?.RELAYFILE_BASE_URL ??
|
|
5523
|
+
process.env.RELAYFILE_BASE_URL ??
|
|
5524
|
+
'http://127.0.0.1:8080';
|
|
4980
5525
|
|
|
4981
5526
|
try {
|
|
4982
5527
|
const {
|
|
@@ -4984,10 +5529,17 @@ export class WorkflowRunner {
|
|
|
4984
5529
|
exitCode,
|
|
4985
5530
|
exitSignal,
|
|
4986
5531
|
} = await new Promise<{ stdout: string; exitCode?: number; exitSignal?: string }>((resolve, reject) => {
|
|
5532
|
+
const spawnEnv =
|
|
5533
|
+
agentDef.cli === 'opencode'
|
|
5534
|
+
? {
|
|
5535
|
+
...env,
|
|
5536
|
+
OPENCODE_PERMISSION: JSON.stringify({ '*': 'allow', external_directory: { '*': 'allow' } }),
|
|
5537
|
+
}
|
|
5538
|
+
: env;
|
|
4987
5539
|
const child = spawnProcess([cmd, ...args], {
|
|
4988
5540
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4989
|
-
cwd: this.
|
|
4990
|
-
env:
|
|
5541
|
+
cwd: this.resolveExecutionCwd(step, agentDef),
|
|
5542
|
+
env: spawnEnv,
|
|
4991
5543
|
});
|
|
4992
5544
|
|
|
4993
5545
|
// Update workers.json with PID now that we have it
|
|
@@ -5189,7 +5741,7 @@ export class WorkflowRunner {
|
|
|
5189
5741
|
let ptyChunks: string[] = [];
|
|
5190
5742
|
|
|
5191
5743
|
try {
|
|
5192
|
-
const agentCwd = this.
|
|
5744
|
+
const agentCwd = this.resolveExecutionCwd(step, agentDef);
|
|
5193
5745
|
const interactiveSpawnPolicy = resolveSpawnPolicy({
|
|
5194
5746
|
AGENT_NAME: agentName,
|
|
5195
5747
|
AGENT_CLI: agentDef.cli,
|
|
@@ -5205,6 +5757,7 @@ export class WorkflowRunner {
|
|
|
5205
5757
|
task: preparedTask.spawnTaskText,
|
|
5206
5758
|
idleThresholdSecs: agentDef.constraints?.idleThresholdSecs,
|
|
5207
5759
|
cwd: agentCwd,
|
|
5760
|
+
agentToken: this.agentTokens.get(agentDef.name),
|
|
5208
5761
|
});
|
|
5209
5762
|
|
|
5210
5763
|
// Re-key PTY maps if broker assigned a different name than requested
|