@action-llama/action-llama 0.2.0 → 0.4.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/LICENSE +21 -0
- package/README.md +61 -90
- package/dist/agents/container-entry.js +183 -43
- package/dist/agents/container-entry.js.map +1 -1
- package/dist/agents/container-runner.d.ts +11 -4
- package/dist/agents/container-runner.d.ts.map +1 -1
- package/dist/agents/container-runner.js +107 -99
- package/dist/agents/container-runner.js.map +1 -1
- package/dist/agents/prompt.d.ts +2 -0
- package/dist/agents/prompt.d.ts.map +1 -1
- package/dist/agents/prompt.js +18 -10
- package/dist/agents/prompt.js.map +1 -1
- package/dist/agents/runner.d.ts +10 -1
- package/dist/agents/runner.d.ts.map +1 -1
- package/dist/agents/runner.js +95 -9
- package/dist/agents/runner.js.map +1 -1
- package/dist/cli/commands/cloud-setup.d.ts +4 -0
- package/dist/cli/commands/cloud-setup.d.ts.map +1 -0
- package/dist/cli/commands/cloud-setup.js +565 -0
- package/dist/cli/commands/cloud-setup.js.map +1 -0
- package/dist/cli/commands/cloud-teardown.d.ts +6 -0
- package/dist/cli/commands/cloud-teardown.d.ts.map +1 -0
- package/dist/cli/commands/cloud-teardown.js +152 -0
- package/dist/cli/commands/cloud-teardown.js.map +1 -0
- package/dist/cli/commands/{setup.d.ts → console.d.ts} +1 -1
- package/dist/cli/commands/console.d.ts.map +1 -0
- package/dist/cli/commands/console.js +273 -0
- package/dist/cli/commands/console.js.map +1 -0
- package/dist/cli/commands/creds.d.ts +2 -0
- package/dist/cli/commands/creds.d.ts.map +1 -0
- package/dist/cli/commands/creds.js +62 -0
- package/dist/cli/commands/creds.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +7 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +405 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/logs.d.ts +1 -0
- package/dist/cli/commands/logs.d.ts.map +1 -1
- package/dist/cli/commands/logs.js +67 -0
- package/dist/cli/commands/logs.js.map +1 -1
- package/dist/cli/commands/new.d.ts.map +1 -1
- package/dist/cli/commands/new.js +30 -28
- package/dist/cli/commands/new.js.map +1 -1
- package/dist/cli/commands/run.d.ts +6 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +121 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/start.d.ts +2 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +41 -14
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.d.ts +1 -0
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +39 -2
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/main.js +61 -12
- package/dist/cli/main.js.map +1 -1
- package/dist/credentials/builtins/anthropic-key.d.ts.map +1 -1
- package/dist/credentials/builtins/anthropic-key.js +3 -4
- package/dist/credentials/builtins/anthropic-key.js.map +1 -1
- package/dist/credentials/builtins/aws.d.ts +4 -0
- package/dist/credentials/builtins/aws.d.ts.map +1 -0
- package/dist/credentials/builtins/aws.js +33 -0
- package/dist/credentials/builtins/aws.js.map +1 -0
- package/dist/credentials/builtins/bugsnag-token.d.ts +4 -0
- package/dist/credentials/builtins/bugsnag-token.d.ts.map +1 -0
- package/dist/credentials/builtins/bugsnag-token.js +18 -0
- package/dist/credentials/builtins/bugsnag-token.js.map +1 -0
- package/dist/credentials/builtins/github-token.d.ts.map +1 -1
- package/dist/credentials/builtins/github-token.js +1 -2
- package/dist/credentials/builtins/github-token.js.map +1 -1
- package/dist/credentials/builtins/github-webhook-secret.js +3 -3
- package/dist/credentials/builtins/github-webhook-secret.js.map +1 -1
- package/dist/credentials/builtins/id-rsa.d.ts +2 -2
- package/dist/credentials/builtins/id-rsa.d.ts.map +1 -1
- package/dist/credentials/builtins/id-rsa.js +75 -47
- package/dist/credentials/builtins/id-rsa.js.map +1 -1
- package/dist/credentials/builtins/index.d.ts.map +1 -1
- package/dist/credentials/builtins/index.js +17 -7
- package/dist/credentials/builtins/index.js.map +1 -1
- package/dist/credentials/builtins/netlify-token.d.ts +4 -0
- package/dist/credentials/builtins/netlify-token.d.ts.map +1 -0
- package/dist/credentials/builtins/netlify-token.js +18 -0
- package/dist/credentials/builtins/netlify-token.js.map +1 -0
- package/dist/credentials/builtins/openai-key.d.ts +4 -0
- package/dist/credentials/builtins/openai-key.d.ts.map +1 -0
- package/dist/credentials/builtins/openai-key.js +38 -0
- package/dist/credentials/builtins/openai-key.js.map +1 -0
- package/dist/credentials/builtins/sentry-client-secret.d.ts.map +1 -1
- package/dist/credentials/builtins/sentry-client-secret.js +1 -2
- package/dist/credentials/builtins/sentry-client-secret.js.map +1 -1
- package/dist/credentials/builtins/sentry-token.d.ts.map +1 -1
- package/dist/credentials/builtins/sentry-token.js +2 -3
- package/dist/credentials/builtins/sentry-token.js.map +1 -1
- package/dist/credentials/builtins/x-twitter-api.d.ts +4 -0
- package/dist/credentials/builtins/x-twitter-api.d.ts.map +1 -0
- package/dist/credentials/builtins/x-twitter-api.js +28 -0
- package/dist/credentials/builtins/x-twitter-api.js.map +1 -0
- package/dist/credentials/prompter.d.ts +1 -1
- package/dist/credentials/prompter.d.ts.map +1 -1
- package/dist/credentials/prompter.js +14 -21
- package/dist/credentials/prompter.js.map +1 -1
- package/dist/credentials/schema.d.ts +0 -1
- package/dist/credentials/schema.d.ts.map +1 -1
- package/dist/credentials/schema.js +2 -3
- package/dist/credentials/schema.js.map +1 -1
- package/dist/docker/cloud-run-runtime.d.ts +61 -0
- package/dist/docker/cloud-run-runtime.d.ts.map +1 -0
- package/dist/docker/cloud-run-runtime.js +510 -0
- package/dist/docker/cloud-run-runtime.js.map +1 -0
- package/dist/docker/ecs-runtime.d.ts +73 -0
- package/dist/docker/ecs-runtime.d.ts.map +1 -0
- package/dist/docker/ecs-runtime.js +596 -0
- package/dist/docker/ecs-runtime.js.map +1 -0
- package/dist/docker/image.d.ts +8 -0
- package/dist/docker/image.d.ts.map +1 -1
- package/dist/docker/image.js +28 -3
- package/dist/docker/image.js.map +1 -1
- package/dist/docker/local-runtime.d.ts +19 -0
- package/dist/docker/local-runtime.d.ts.map +1 -0
- package/dist/docker/local-runtime.js +209 -0
- package/dist/docker/local-runtime.js.map +1 -0
- package/dist/docker/network.d.ts +1 -1
- package/dist/docker/network.d.ts.map +1 -1
- package/dist/docker/network.js +2 -1
- package/dist/docker/network.js.map +1 -1
- package/dist/docker/runtime.d.ts +90 -0
- package/dist/docker/runtime.d.ts.map +1 -0
- package/dist/docker/runtime.js +2 -0
- package/dist/docker/runtime.js.map +1 -0
- package/dist/gateway/index.d.ts +8 -2
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js +16 -8
- package/dist/gateway/index.js.map +1 -1
- package/dist/gateway/routes/credentials.d.ts +5 -0
- package/dist/gateway/routes/credentials.d.ts.map +1 -0
- package/dist/gateway/routes/credentials.js +17 -0
- package/dist/gateway/routes/credentials.js.map +1 -0
- package/dist/gateway/routes/logs.d.ts +5 -0
- package/dist/gateway/routes/logs.d.ts.map +1 -0
- package/dist/gateway/routes/logs.js +31 -0
- package/dist/gateway/routes/logs.js.map +1 -0
- package/dist/gateway/routes/shutdown.d.ts +2 -1
- package/dist/gateway/routes/shutdown.d.ts.map +1 -1
- package/dist/gateway/routes/shutdown.js +7 -16
- package/dist/gateway/routes/shutdown.js.map +1 -1
- package/dist/gateway/routes/webhooks.d.ts +2 -1
- package/dist/gateway/routes/webhooks.d.ts.map +1 -1
- package/dist/gateway/routes/webhooks.js +11 -4
- package/dist/gateway/routes/webhooks.js.map +1 -1
- package/dist/gateway/types.d.ts +6 -0
- package/dist/gateway/types.d.ts.map +1 -0
- package/dist/gateway/types.js +2 -0
- package/dist/gateway/types.js.map +1 -0
- package/dist/scheduler/index.d.ts +3 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +299 -59
- package/dist/scheduler/index.js.map +1 -1
- package/dist/setup/prompts.d.ts.map +1 -1
- package/dist/setup/prompts.js +14 -21
- package/dist/setup/prompts.js.map +1 -1
- package/dist/setup/scaffold.d.ts +2 -2
- package/dist/setup/scaffold.d.ts.map +1 -1
- package/dist/setup/scaffold.js +369 -27
- package/dist/setup/scaffold.js.map +1 -1
- package/dist/setup/validators.d.ts +14 -0
- package/dist/setup/validators.d.ts.map +1 -1
- package/dist/setup/validators.js +53 -0
- package/dist/setup/validators.js.map +1 -1
- package/dist/shared/asm-backend.d.ts +25 -0
- package/dist/shared/asm-backend.d.ts.map +1 -0
- package/dist/shared/asm-backend.js +107 -0
- package/dist/shared/asm-backend.js.map +1 -0
- package/dist/shared/aws-constants.d.ts +55 -0
- package/dist/shared/aws-constants.d.ts.map +1 -0
- package/dist/shared/aws-constants.js +55 -0
- package/dist/shared/aws-constants.js.map +1 -0
- package/dist/shared/config.d.ts +25 -5
- package/dist/shared/config.d.ts.map +1 -1
- package/dist/shared/config.js +15 -22
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/credential-backend.d.ts +28 -0
- package/dist/shared/credential-backend.d.ts.map +1 -0
- package/dist/shared/credential-backend.js +2 -0
- package/dist/shared/credential-backend.js.map +1 -0
- package/dist/shared/credentials.d.ts +75 -5
- package/dist/shared/credentials.d.ts.map +1 -1
- package/dist/shared/credentials.js +141 -24
- package/dist/shared/credentials.js.map +1 -1
- package/dist/shared/filesystem-backend.d.ts +18 -0
- package/dist/shared/filesystem-backend.d.ts.map +1 -0
- package/dist/shared/filesystem-backend.js +86 -0
- package/dist/shared/filesystem-backend.js.map +1 -0
- package/dist/shared/git.js +1 -1
- package/dist/shared/git.js.map +1 -1
- package/dist/shared/gsm-backend.d.ts +35 -0
- package/dist/shared/gsm-backend.d.ts.map +1 -0
- package/dist/shared/gsm-backend.js +208 -0
- package/dist/shared/gsm-backend.js.map +1 -0
- package/dist/shared/remote.d.ts +11 -0
- package/dist/shared/remote.d.ts.map +1 -0
- package/dist/shared/remote.js +29 -0
- package/dist/shared/remote.js.map +1 -0
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +22 -7
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/status-tracker.d.ts +6 -3
- package/dist/tui/status-tracker.d.ts.map +1 -1
- package/dist/tui/status-tracker.js +14 -2
- package/dist/tui/status-tracker.js.map +1 -1
- package/dist/webhooks/definitions/github.js +1 -1
- package/dist/webhooks/definitions/sentry.js +1 -1
- package/dist/webhooks/providers/github.d.ts +1 -1
- package/dist/webhooks/providers/github.d.ts.map +1 -1
- package/dist/webhooks/providers/github.js +13 -9
- package/dist/webhooks/providers/github.js.map +1 -1
- package/dist/webhooks/providers/sentry.d.ts +1 -1
- package/dist/webhooks/providers/sentry.d.ts.map +1 -1
- package/dist/webhooks/providers/sentry.js +12 -9
- package/dist/webhooks/providers/sentry.js.map +1 -1
- package/dist/webhooks/registry.d.ts +1 -1
- package/dist/webhooks/registry.d.ts.map +1 -1
- package/dist/webhooks/registry.js +20 -13
- package/dist/webhooks/registry.js.map +1 -1
- package/dist/webhooks/types.d.ts +16 -6
- package/dist/webhooks/types.d.ts.map +1 -1
- package/docker/Dockerfile +4 -11
- package/package.json +12 -3
- package/dist/cli/commands/setup.d.ts.map +0 -1
- package/dist/cli/commands/setup.js +0 -60
- package/dist/cli/commands/setup.js.map +0 -1
- package/dist/docker/container.d.ts +0 -19
- package/dist/docker/container.d.ts.map +0 -1
- package/dist/docker/container.js +0 -73
- package/dist/docker/container.js.map +0 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ContainerRuntime, RuntimeLaunchOpts, RuntimeCredentials, BuildImageOpts, RunningAgent } from "./runtime.js";
|
|
2
|
+
export interface ECSFargateConfig {
|
|
3
|
+
awsRegion: string;
|
|
4
|
+
ecsCluster: string;
|
|
5
|
+
ecrRepository: string;
|
|
6
|
+
executionRoleArn: string;
|
|
7
|
+
taskRoleArn: string;
|
|
8
|
+
subnets: string[];
|
|
9
|
+
securityGroups?: string[];
|
|
10
|
+
secretPrefix?: string;
|
|
11
|
+
buildBucket?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* AWS ECS Fargate runtime.
|
|
15
|
+
*
|
|
16
|
+
* Launches agents as ECS Fargate tasks with AWS Secrets Manager secrets
|
|
17
|
+
* injected as environment variables or mounted via container definitions.
|
|
18
|
+
*
|
|
19
|
+
* Auth: AWS SDK default credential provider chain
|
|
20
|
+
* 1. AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY env vars
|
|
21
|
+
* 2. AWS_PROFILE / ~/.aws/credentials
|
|
22
|
+
* 3. SSO / IAM instance role (EC2/ECS/Lambda)
|
|
23
|
+
*
|
|
24
|
+
* The runtime credentials (your machine) need:
|
|
25
|
+
* - ecs:RegisterTaskDefinition, ecs:RunTask, ecs:DescribeTasks, ecs:StopTask
|
|
26
|
+
* - ecr:GetAuthorizationToken, ecr:BatchCheckLayerAvailability, ecr:PutImage, etc.
|
|
27
|
+
* - logs:GetLogEvents
|
|
28
|
+
*
|
|
29
|
+
* The task role (container) needs per-agent:
|
|
30
|
+
* - secretsmanager:GetSecretValue on its specific secrets
|
|
31
|
+
*/
|
|
32
|
+
export declare class ECSFargateRuntime implements ContainerRuntime {
|
|
33
|
+
readonly needsGateway = false;
|
|
34
|
+
private config;
|
|
35
|
+
private prefix;
|
|
36
|
+
private ecsClient;
|
|
37
|
+
private smClient;
|
|
38
|
+
private logsClient;
|
|
39
|
+
private cbClient;
|
|
40
|
+
private s3Client;
|
|
41
|
+
private ecrClient;
|
|
42
|
+
constructor(config: ECSFargateConfig);
|
|
43
|
+
private static readonly STARTED_BY_PREFIX;
|
|
44
|
+
static readonly LOG_GROUP: string;
|
|
45
|
+
private logGroupCreated;
|
|
46
|
+
private ensureLogGroup;
|
|
47
|
+
isAgentRunning(agentName: string): Promise<boolean>;
|
|
48
|
+
listRunningAgents(): Promise<RunningAgent[]>;
|
|
49
|
+
prepareCredentials(credRefs: string[]): Promise<RuntimeCredentials>;
|
|
50
|
+
cleanupCredentials(_creds: RuntimeCredentials): void;
|
|
51
|
+
buildImage(opts: BuildImageOpts): Promise<string>;
|
|
52
|
+
pushImage(_localImage: string): Promise<string>;
|
|
53
|
+
private buildImageCodeBuild;
|
|
54
|
+
private ecrImageExists;
|
|
55
|
+
private ensureBuildBucket;
|
|
56
|
+
private ensureCodeBuildProject;
|
|
57
|
+
launch(opts: RuntimeLaunchOpts): Promise<string>;
|
|
58
|
+
streamLogs(taskArn: string, onLine: (line: string) => void, onStderr?: (text: string) => void): {
|
|
59
|
+
stop: () => void;
|
|
60
|
+
};
|
|
61
|
+
waitForExit(taskArn: string, timeoutSeconds: number): Promise<number>;
|
|
62
|
+
kill(taskArn: string): Promise<void>;
|
|
63
|
+
remove(_taskArn: string): Promise<void>;
|
|
64
|
+
fetchLogs(agentName: string, limit: number): Promise<string[]>;
|
|
65
|
+
private awsSecretName;
|
|
66
|
+
private listSecretFields;
|
|
67
|
+
private registerTaskDefinition;
|
|
68
|
+
private runTask;
|
|
69
|
+
private getLogEvents;
|
|
70
|
+
private deriveTaskRoleArn;
|
|
71
|
+
private getAccountId;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=ecs-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ecs-runtime.d.ts","sourceRoot":"","sources":["../../src/docker/ecs-runtime.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,kBAAkB,EAAe,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAIvI,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,iBAAkB,YAAW,gBAAgB;IACxD,QAAQ,CAAC,YAAY,SAAS;IAE9B,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,SAAS,CAAY;gBAEjB,MAAM,EAAE,gBAAgB;IAapC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAA4B;IACrE,MAAM,CAAC,QAAQ,CAAC,SAAS,SAA2B;IACpD,OAAO,CAAC,eAAe,CAAS;YAElB,cAAc;IAYtB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUnD,iBAAiB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IA6B5C,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAmBzE,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI;IAM9C,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;IAIjD,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAKvC,mBAAmB;YAmMnB,cAAc;YAgBd,iBAAiB;YAuBjB,sBAAsB;IA2C9B,MAAM,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;IA0BtD,UAAU,CACR,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAC9B,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAChC;QAAE,IAAI,EAAE,MAAM,IAAI,CAAA;KAAE;IAwCjB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwBrE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYpC,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAcpE,OAAO,CAAC,aAAa;YAIP,gBAAgB;YAoBhB,sBAAsB;YAwDtB,OAAO;YA0BP,YAAY;IAoB1B,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,YAAY;CAKrB"}
|
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
import { createReadStream } from "fs";
|
|
3
|
+
import { ECSClient, RegisterTaskDefinitionCommand, RunTaskCommand, DescribeTasksCommand, StopTaskCommand, ListTasksCommand, } from "@aws-sdk/client-ecs";
|
|
4
|
+
import { SecretsManagerClient, ListSecretsCommand, } from "@aws-sdk/client-secrets-manager";
|
|
5
|
+
import { CloudWatchLogsClient, GetLogEventsCommand, FilterLogEventsCommand, CreateLogGroupCommand, } from "@aws-sdk/client-cloudwatch-logs";
|
|
6
|
+
import { CodeBuildClient, StartBuildCommand, BatchGetBuildsCommand, CreateProjectCommand, } from "@aws-sdk/client-codebuild";
|
|
7
|
+
import { S3Client, PutObjectCommand, CreateBucketCommand, HeadBucketCommand, } from "@aws-sdk/client-s3";
|
|
8
|
+
import { ECRClient, BatchGetImageCommand, } from "@aws-sdk/client-ecr";
|
|
9
|
+
import { parseCredentialRef } from "../shared/credentials.js";
|
|
10
|
+
import { AWS_CONSTANTS } from "../shared/aws-constants.js";
|
|
11
|
+
/**
|
|
12
|
+
* AWS ECS Fargate runtime.
|
|
13
|
+
*
|
|
14
|
+
* Launches agents as ECS Fargate tasks with AWS Secrets Manager secrets
|
|
15
|
+
* injected as environment variables or mounted via container definitions.
|
|
16
|
+
*
|
|
17
|
+
* Auth: AWS SDK default credential provider chain
|
|
18
|
+
* 1. AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY env vars
|
|
19
|
+
* 2. AWS_PROFILE / ~/.aws/credentials
|
|
20
|
+
* 3. SSO / IAM instance role (EC2/ECS/Lambda)
|
|
21
|
+
*
|
|
22
|
+
* The runtime credentials (your machine) need:
|
|
23
|
+
* - ecs:RegisterTaskDefinition, ecs:RunTask, ecs:DescribeTasks, ecs:StopTask
|
|
24
|
+
* - ecr:GetAuthorizationToken, ecr:BatchCheckLayerAvailability, ecr:PutImage, etc.
|
|
25
|
+
* - logs:GetLogEvents
|
|
26
|
+
*
|
|
27
|
+
* The task role (container) needs per-agent:
|
|
28
|
+
* - secretsmanager:GetSecretValue on its specific secrets
|
|
29
|
+
*/
|
|
30
|
+
export class ECSFargateRuntime {
|
|
31
|
+
needsGateway = false;
|
|
32
|
+
config;
|
|
33
|
+
prefix;
|
|
34
|
+
ecsClient;
|
|
35
|
+
smClient;
|
|
36
|
+
logsClient;
|
|
37
|
+
cbClient;
|
|
38
|
+
s3Client;
|
|
39
|
+
ecrClient;
|
|
40
|
+
constructor(config) {
|
|
41
|
+
this.config = config;
|
|
42
|
+
this.prefix = config.secretPrefix || AWS_CONSTANTS.DEFAULT_SECRET_PREFIX;
|
|
43
|
+
const clientConfig = { region: config.awsRegion };
|
|
44
|
+
this.ecsClient = new ECSClient(clientConfig);
|
|
45
|
+
this.smClient = new SecretsManagerClient(clientConfig);
|
|
46
|
+
this.logsClient = new CloudWatchLogsClient(clientConfig);
|
|
47
|
+
this.cbClient = new CodeBuildClient(clientConfig);
|
|
48
|
+
this.s3Client = new S3Client(clientConfig);
|
|
49
|
+
this.ecrClient = new ECRClient(clientConfig);
|
|
50
|
+
}
|
|
51
|
+
static STARTED_BY_PREFIX = AWS_CONSTANTS.STARTED_BY;
|
|
52
|
+
static LOG_GROUP = AWS_CONSTANTS.LOG_GROUP;
|
|
53
|
+
logGroupCreated = false;
|
|
54
|
+
async ensureLogGroup() {
|
|
55
|
+
if (this.logGroupCreated)
|
|
56
|
+
return;
|
|
57
|
+
try {
|
|
58
|
+
await this.logsClient.send(new CreateLogGroupCommand({ logGroupName: ECSFargateRuntime.LOG_GROUP }));
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (err.name !== "ResourceAlreadyExistsException")
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
this.logGroupCreated = true;
|
|
65
|
+
}
|
|
66
|
+
// --- Agent tracking ---
|
|
67
|
+
async isAgentRunning(agentName) {
|
|
68
|
+
const family = AWS_CONSTANTS.agentFamily(agentName);
|
|
69
|
+
const res = await this.ecsClient.send(new ListTasksCommand({
|
|
70
|
+
cluster: this.config.ecsCluster,
|
|
71
|
+
family,
|
|
72
|
+
desiredStatus: "RUNNING",
|
|
73
|
+
}));
|
|
74
|
+
return (res.taskArns?.length ?? 0) > 0;
|
|
75
|
+
}
|
|
76
|
+
async listRunningAgents() {
|
|
77
|
+
const res = await this.ecsClient.send(new ListTasksCommand({
|
|
78
|
+
cluster: this.config.ecsCluster,
|
|
79
|
+
startedBy: ECSFargateRuntime.STARTED_BY_PREFIX,
|
|
80
|
+
desiredStatus: "RUNNING",
|
|
81
|
+
}));
|
|
82
|
+
const taskArns = res.taskArns ?? [];
|
|
83
|
+
if (taskArns.length === 0)
|
|
84
|
+
return [];
|
|
85
|
+
const desc = await this.ecsClient.send(new DescribeTasksCommand({
|
|
86
|
+
cluster: this.config.ecsCluster,
|
|
87
|
+
tasks: taskArns,
|
|
88
|
+
}));
|
|
89
|
+
return (desc.tasks ?? []).map((task) => {
|
|
90
|
+
const family = task.taskDefinitionArn?.split("/").pop()?.split(":")[0] ?? "";
|
|
91
|
+
const agentName = AWS_CONSTANTS.agentNameFromFamily(family);
|
|
92
|
+
return {
|
|
93
|
+
agentName,
|
|
94
|
+
taskId: task.taskArn?.split("/").pop() ?? task.taskArn ?? "unknown",
|
|
95
|
+
status: task.lastStatus ?? "UNKNOWN",
|
|
96
|
+
startedAt: task.startedAt,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// --- Credential preparation ---
|
|
101
|
+
async prepareCredentials(credRefs) {
|
|
102
|
+
const mounts = [];
|
|
103
|
+
for (const credRef of credRefs) {
|
|
104
|
+
const { type, instance } = parseCredentialRef(credRef);
|
|
105
|
+
const fields = await this.listSecretFields(type, instance);
|
|
106
|
+
for (const field of fields) {
|
|
107
|
+
const secretName = this.awsSecretName(type, instance, field);
|
|
108
|
+
mounts.push({
|
|
109
|
+
secretId: secretName,
|
|
110
|
+
mountPath: `/credentials/${type}/${instance}/${field}`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { strategy: "secrets-manager", mounts };
|
|
115
|
+
}
|
|
116
|
+
cleanupCredentials(_creds) {
|
|
117
|
+
// No-op
|
|
118
|
+
}
|
|
119
|
+
// --- Image management ---
|
|
120
|
+
async buildImage(opts) {
|
|
121
|
+
return this.buildImageCodeBuild(opts, opts.onProgress);
|
|
122
|
+
}
|
|
123
|
+
async pushImage(_localImage) {
|
|
124
|
+
// CodeBuild handles build + push in one step; the image is already in ECR
|
|
125
|
+
return `${this.config.ecrRepository}:${_localImage.replace(":", "-")}`;
|
|
126
|
+
}
|
|
127
|
+
async buildImageCodeBuild(opts, onProgress) {
|
|
128
|
+
onProgress?.("Preparing build context");
|
|
129
|
+
const { join, relative, isAbsolute } = await import("path");
|
|
130
|
+
const { readFileSync, writeFileSync } = await import("fs");
|
|
131
|
+
const { randomUUID, createHash } = await import("crypto");
|
|
132
|
+
// Resolve the Dockerfile path and handle two cases:
|
|
133
|
+
// 1. Dockerfile is outside the context dir (agent custom Dockerfiles) — copy it in
|
|
134
|
+
// 2. Dockerfile references a local base image — rewrite FROM to use the remote ECR URI
|
|
135
|
+
const resolvedDockerfile = isAbsolute(opts.dockerfile)
|
|
136
|
+
? opts.dockerfile
|
|
137
|
+
: join(opts.contextDir, opts.dockerfile);
|
|
138
|
+
const relPath = relative(opts.contextDir, resolvedDockerfile);
|
|
139
|
+
let dockerfileTar;
|
|
140
|
+
let tempDockerfile;
|
|
141
|
+
const needsCopy = relPath.startsWith("..");
|
|
142
|
+
const needsRewrite = !!opts.baseImage;
|
|
143
|
+
if (needsCopy || needsRewrite) {
|
|
144
|
+
tempDockerfile = join(opts.contextDir, `.Dockerfile.${randomUUID().slice(0, 8)}`);
|
|
145
|
+
let content = readFileSync(resolvedDockerfile, "utf-8");
|
|
146
|
+
if (needsRewrite && opts.baseImage) {
|
|
147
|
+
content = content.replace(/^FROM\s+\S+/m, `FROM ${opts.baseImage}`);
|
|
148
|
+
}
|
|
149
|
+
writeFileSync(tempDockerfile, content);
|
|
150
|
+
dockerfileTar = relative(opts.contextDir, tempDockerfile);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
dockerfileTar = relPath;
|
|
154
|
+
}
|
|
155
|
+
// Hash individual file contents (sorted) for a deterministic fingerprint.
|
|
156
|
+
const { readdirSync, readFileSync: readFileSyncBuf } = await import("fs");
|
|
157
|
+
const hash = createHash("sha256");
|
|
158
|
+
const hashFile = (p) => {
|
|
159
|
+
hash.update(p);
|
|
160
|
+
hash.update(readFileSyncBuf(join(opts.contextDir, p)));
|
|
161
|
+
};
|
|
162
|
+
const hashDir = (dir) => {
|
|
163
|
+
const entries = readdirSync(join(opts.contextDir, dir), { withFileTypes: true })
|
|
164
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
const p = join(dir, entry.name);
|
|
167
|
+
if (entry.isDirectory())
|
|
168
|
+
hashDir(p);
|
|
169
|
+
else
|
|
170
|
+
hashFile(p);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
hashFile(dockerfileTar);
|
|
174
|
+
hashFile("package.json");
|
|
175
|
+
hashDir("dist");
|
|
176
|
+
// Clean up temp Dockerfile after hashing but before any early returns
|
|
177
|
+
if (tempDockerfile) {
|
|
178
|
+
try {
|
|
179
|
+
const { rmSync } = await import("fs");
|
|
180
|
+
rmSync(tempDockerfile);
|
|
181
|
+
}
|
|
182
|
+
catch { }
|
|
183
|
+
}
|
|
184
|
+
const contentHash = hash.digest("hex").slice(0, 16);
|
|
185
|
+
const nameTag = opts.tag.replace(":", "-");
|
|
186
|
+
const hashTag = `${nameTag}-${contentHash}`;
|
|
187
|
+
const remoteTag = `${this.config.ecrRepository}:${hashTag}`;
|
|
188
|
+
// Check ECR cache before doing any S3/CodeBuild work
|
|
189
|
+
onProgress?.(`Checking cache (${hashTag})`);
|
|
190
|
+
const repoName = this.config.ecrRepository.split("/").pop();
|
|
191
|
+
const imageExists = await this.ecrImageExists(repoName, hashTag, onProgress);
|
|
192
|
+
if (imageExists) {
|
|
193
|
+
onProgress?.("Image unchanged — reusing cached build");
|
|
194
|
+
return remoteTag;
|
|
195
|
+
}
|
|
196
|
+
// Cache miss — need to build. Re-create temp Dockerfile for tarball.
|
|
197
|
+
if (needsCopy || needsRewrite) {
|
|
198
|
+
tempDockerfile = join(opts.contextDir, `.Dockerfile.${randomUUID().slice(0, 8)}`);
|
|
199
|
+
let content = readFileSync(resolvedDockerfile, "utf-8");
|
|
200
|
+
if (needsRewrite && opts.baseImage) {
|
|
201
|
+
content = content.replace(/^FROM\s+\S+/m, `FROM ${opts.baseImage}`);
|
|
202
|
+
}
|
|
203
|
+
writeFileSync(tempDockerfile, content);
|
|
204
|
+
dockerfileTar = relative(opts.contextDir, tempDockerfile);
|
|
205
|
+
}
|
|
206
|
+
const { tmpdir } = await import("os");
|
|
207
|
+
const tarPath = join(tmpdir(), `${AWS_CONSTANTS.BUILD_S3_PREFIX}-${randomUUID().slice(0, 8)}.tar.gz`);
|
|
208
|
+
try {
|
|
209
|
+
execFileSync("tar", [
|
|
210
|
+
"czf", tarPath,
|
|
211
|
+
"-C", opts.contextDir,
|
|
212
|
+
dockerfileTar, "package.json", "dist",
|
|
213
|
+
], {
|
|
214
|
+
encoding: "utf-8",
|
|
215
|
+
timeout: 60_000,
|
|
216
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
if (tempDockerfile) {
|
|
221
|
+
try {
|
|
222
|
+
const { rmSync } = await import("fs");
|
|
223
|
+
rmSync(tempDockerfile);
|
|
224
|
+
}
|
|
225
|
+
catch { }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Upload to S3
|
|
229
|
+
onProgress?.("Uploading to S3");
|
|
230
|
+
const bucket = await this.ensureBuildBucket();
|
|
231
|
+
const s3Key = `${AWS_CONSTANTS.BUILD_S3_PREFIX}/${nameTag}-${Date.now()}.tar.gz`;
|
|
232
|
+
await this.s3Client.send(new PutObjectCommand({
|
|
233
|
+
Bucket: bucket,
|
|
234
|
+
Key: s3Key,
|
|
235
|
+
Body: createReadStream(tarPath),
|
|
236
|
+
}));
|
|
237
|
+
// Clean up local tarball
|
|
238
|
+
try {
|
|
239
|
+
const { rmSync } = await import("fs");
|
|
240
|
+
rmSync(tarPath);
|
|
241
|
+
}
|
|
242
|
+
catch { }
|
|
243
|
+
// Ensure CodeBuild project exists
|
|
244
|
+
const projectName = AWS_CONSTANTS.CODEBUILD_PROJECT;
|
|
245
|
+
await this.ensureCodeBuildProject(projectName, bucket);
|
|
246
|
+
const registry = this.config.ecrRepository.split("/")[0];
|
|
247
|
+
// Start build
|
|
248
|
+
const buildspec = [
|
|
249
|
+
"version: 0.2",
|
|
250
|
+
"phases:",
|
|
251
|
+
" pre_build:",
|
|
252
|
+
" commands:",
|
|
253
|
+
" - tar xzf *.tar.gz && rm -f *.tar.gz",
|
|
254
|
+
" - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ECR_REGISTRY",
|
|
255
|
+
" build:",
|
|
256
|
+
" commands:",
|
|
257
|
+
" - docker build -t $IMAGE_URI -f $DOCKERFILE .",
|
|
258
|
+
" - docker push $IMAGE_URI",
|
|
259
|
+
].join("\n");
|
|
260
|
+
const buildRes = await this.cbClient.send(new StartBuildCommand({
|
|
261
|
+
projectName,
|
|
262
|
+
sourceTypeOverride: "S3",
|
|
263
|
+
sourceLocationOverride: `${bucket}/${s3Key}`,
|
|
264
|
+
buildspecOverride: buildspec,
|
|
265
|
+
environmentVariablesOverride: [
|
|
266
|
+
{ name: "IMAGE_URI", value: remoteTag },
|
|
267
|
+
{ name: "ECR_REGISTRY", value: registry },
|
|
268
|
+
{ name: "DOCKERFILE", value: dockerfileTar },
|
|
269
|
+
],
|
|
270
|
+
}));
|
|
271
|
+
const buildId = buildRes.build?.id;
|
|
272
|
+
if (!buildId)
|
|
273
|
+
throw new Error("CodeBuild did not return a build ID");
|
|
274
|
+
onProgress?.("Queued — waiting for CodeBuild");
|
|
275
|
+
// Poll until complete
|
|
276
|
+
while (true) {
|
|
277
|
+
await sleep(10_000);
|
|
278
|
+
const status = await this.cbClient.send(new BatchGetBuildsCommand({ ids: [buildId] }));
|
|
279
|
+
const build = status.builds?.[0];
|
|
280
|
+
if (!build)
|
|
281
|
+
throw new Error(`CodeBuild build ${buildId} not found`);
|
|
282
|
+
if (build.currentPhase) {
|
|
283
|
+
const phaseLabels = {
|
|
284
|
+
SUBMITTED: "Submitted",
|
|
285
|
+
QUEUED: "Queued",
|
|
286
|
+
PROVISIONING: "Provisioning build environment",
|
|
287
|
+
DOWNLOAD_SOURCE: "Downloading source",
|
|
288
|
+
INSTALL: "Installing dependencies",
|
|
289
|
+
PRE_BUILD: "Logging in to ECR",
|
|
290
|
+
BUILD: "Building and pushing image",
|
|
291
|
+
POST_BUILD: "Finalizing",
|
|
292
|
+
UPLOAD_ARTIFACTS: "Uploading artifacts",
|
|
293
|
+
FINALIZING: "Finalizing",
|
|
294
|
+
COMPLETED: "Complete",
|
|
295
|
+
};
|
|
296
|
+
const label = phaseLabels[build.currentPhase] || build.currentPhase;
|
|
297
|
+
onProgress?.(label);
|
|
298
|
+
}
|
|
299
|
+
if (build.buildComplete) {
|
|
300
|
+
if (build.buildStatus !== "SUCCEEDED") {
|
|
301
|
+
const logs = build.logs?.deepLink || "";
|
|
302
|
+
throw new Error(`CodeBuild build failed (${build.buildStatus}). Logs: ${logs}`);
|
|
303
|
+
}
|
|
304
|
+
return remoteTag;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async ecrImageExists(repositoryName, imageTag, onProgress) {
|
|
309
|
+
try {
|
|
310
|
+
const res = await this.ecrClient.send(new BatchGetImageCommand({
|
|
311
|
+
repositoryName,
|
|
312
|
+
imageIds: [{ imageTag }],
|
|
313
|
+
}));
|
|
314
|
+
return (res.images?.length ?? 0) > 0;
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
// ImageNotFoundException is expected when the image doesn't exist
|
|
318
|
+
if (err.name === "ImageNotFoundException")
|
|
319
|
+
return false;
|
|
320
|
+
// Surface unexpected errors (permissions, network) so they're not silently swallowed
|
|
321
|
+
onProgress?.(`Cache check failed: ${err.message ?? err}`);
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async ensureBuildBucket() {
|
|
326
|
+
if (this.config.buildBucket) {
|
|
327
|
+
return this.config.buildBucket;
|
|
328
|
+
}
|
|
329
|
+
// Derive bucket name from account ID + region
|
|
330
|
+
const accountId = this.getAccountId();
|
|
331
|
+
const bucket = AWS_CONSTANTS.buildBucket(accountId, this.config.awsRegion);
|
|
332
|
+
try {
|
|
333
|
+
await this.s3Client.send(new HeadBucketCommand({ Bucket: bucket }));
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) {
|
|
337
|
+
await this.s3Client.send(new CreateBucketCommand({ Bucket: bucket }));
|
|
338
|
+
}
|
|
339
|
+
else if (err.name !== "Forbidden") {
|
|
340
|
+
// Forbidden means bucket exists but we don't own it — try anyway
|
|
341
|
+
throw err;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return bucket;
|
|
345
|
+
}
|
|
346
|
+
async ensureCodeBuildProject(projectName, bucket) {
|
|
347
|
+
const accountId = this.getAccountId();
|
|
348
|
+
const region = this.config.awsRegion;
|
|
349
|
+
try {
|
|
350
|
+
const status = await this.cbClient.send(new BatchGetBuildsCommand({ ids: [`${projectName}:dummy`] }));
|
|
351
|
+
// If the project doesn't exist, BatchGetBuilds returns empty — but we need a better check
|
|
352
|
+
// Actually, just try to create and handle the conflict
|
|
353
|
+
void status;
|
|
354
|
+
}
|
|
355
|
+
catch { }
|
|
356
|
+
const serviceRole = `arn:aws:iam::${accountId}:role/${AWS_CONSTANTS.CODEBUILD_ROLE}`;
|
|
357
|
+
try {
|
|
358
|
+
await this.cbClient.send(new CreateProjectCommand({
|
|
359
|
+
name: projectName,
|
|
360
|
+
source: {
|
|
361
|
+
type: "S3",
|
|
362
|
+
location: `${bucket}/`,
|
|
363
|
+
},
|
|
364
|
+
artifacts: { type: "NO_ARTIFACTS" },
|
|
365
|
+
environment: {
|
|
366
|
+
type: "LINUX_CONTAINER",
|
|
367
|
+
computeType: "BUILD_GENERAL1_MEDIUM",
|
|
368
|
+
image: "aws/codebuild/standard:7.0",
|
|
369
|
+
privilegedMode: true,
|
|
370
|
+
environmentVariables: [
|
|
371
|
+
{ name: "IMAGE_URI", value: "placeholder" },
|
|
372
|
+
{ name: "ECR_REGISTRY", value: "placeholder" },
|
|
373
|
+
{ name: "DOCKERFILE", value: "Dockerfile" },
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
serviceRole,
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
if (err.name !== "ResourceAlreadyExistsException") {
|
|
381
|
+
throw err;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// --- Container lifecycle ---
|
|
386
|
+
async launch(opts) {
|
|
387
|
+
await this.ensureLogGroup();
|
|
388
|
+
const family = AWS_CONSTANTS.agentFamily(opts.agentName);
|
|
389
|
+
const secretMounts = opts.credentials.strategy === "secrets-manager"
|
|
390
|
+
? opts.credentials.mounts
|
|
391
|
+
: [];
|
|
392
|
+
const perAgentRole = this.deriveTaskRoleArn(opts.agentName);
|
|
393
|
+
const taskRoleArn = opts.serviceAccount || perAgentRole || this.config.taskRoleArn;
|
|
394
|
+
const taskDefArn = await this.registerTaskDefinition(family, {
|
|
395
|
+
image: opts.image,
|
|
396
|
+
env: opts.env,
|
|
397
|
+
secretMounts,
|
|
398
|
+
memory: opts.memory || "4096",
|
|
399
|
+
cpus: String((opts.cpus || 2) * 1024),
|
|
400
|
+
taskRoleArn,
|
|
401
|
+
streamPrefix: family,
|
|
402
|
+
});
|
|
403
|
+
const taskArn = await this.runTask(taskDefArn);
|
|
404
|
+
return taskArn;
|
|
405
|
+
}
|
|
406
|
+
streamLogs(taskArn, onLine, onStderr) {
|
|
407
|
+
let stopped = false;
|
|
408
|
+
let nextToken;
|
|
409
|
+
const poll = async () => {
|
|
410
|
+
const taskId = taskArn.split("/").pop();
|
|
411
|
+
const logGroup = ECSFargateRuntime.LOG_GROUP;
|
|
412
|
+
// Describe the task to get the family (used as stream prefix)
|
|
413
|
+
const desc = await this.ecsClient.send(new DescribeTasksCommand({
|
|
414
|
+
cluster: this.config.ecsCluster,
|
|
415
|
+
tasks: [taskArn],
|
|
416
|
+
}));
|
|
417
|
+
const family = desc.tasks?.[0]?.taskDefinitionArn?.split("/").pop()?.split(":")[0] ?? AWS_CONSTANTS.agentFamily("agent");
|
|
418
|
+
// awslogs stream format: <prefix>/<container-name>/<task-id>
|
|
419
|
+
const logStream = `${family}/agent/${taskId}`;
|
|
420
|
+
while (!stopped) {
|
|
421
|
+
try {
|
|
422
|
+
const result = await this.getLogEvents(logGroup, logStream, nextToken);
|
|
423
|
+
for (const event of result.events) {
|
|
424
|
+
onLine(event.message);
|
|
425
|
+
}
|
|
426
|
+
if (result.nextForwardToken) {
|
|
427
|
+
nextToken = result.nextForwardToken;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
if (!stopped && onStderr && err.name !== "ResourceNotFoundException") {
|
|
432
|
+
onStderr(`Log polling error: ${err.message}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (!stopped)
|
|
436
|
+
await sleep(5000);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
poll();
|
|
440
|
+
return { stop: () => { stopped = true; } };
|
|
441
|
+
}
|
|
442
|
+
async waitForExit(taskArn, timeoutSeconds) {
|
|
443
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
444
|
+
while (Date.now() < deadline) {
|
|
445
|
+
const res = await this.ecsClient.send(new DescribeTasksCommand({
|
|
446
|
+
cluster: this.config.ecsCluster,
|
|
447
|
+
tasks: [taskArn],
|
|
448
|
+
}));
|
|
449
|
+
const task = res.tasks?.[0];
|
|
450
|
+
if (!task)
|
|
451
|
+
throw new Error(`Task ${taskArn} not found`);
|
|
452
|
+
if (task.lastStatus === "STOPPED") {
|
|
453
|
+
const exitCode = task.containers?.[0]?.exitCode;
|
|
454
|
+
return exitCode ?? 1;
|
|
455
|
+
}
|
|
456
|
+
await sleep(10_000);
|
|
457
|
+
}
|
|
458
|
+
await this.kill(taskArn);
|
|
459
|
+
throw new Error(`ECS task ${taskArn} timed out after ${timeoutSeconds}s`);
|
|
460
|
+
}
|
|
461
|
+
async kill(taskArn) {
|
|
462
|
+
try {
|
|
463
|
+
await this.ecsClient.send(new StopTaskCommand({
|
|
464
|
+
cluster: this.config.ecsCluster,
|
|
465
|
+
task: taskArn,
|
|
466
|
+
reason: "action-llama timeout",
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// Task may already be stopped
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async remove(_taskArn) {
|
|
474
|
+
// ECS cleans up stopped tasks automatically
|
|
475
|
+
}
|
|
476
|
+
async fetchLogs(agentName, limit) {
|
|
477
|
+
const res = await this.logsClient.send(new FilterLogEventsCommand({
|
|
478
|
+
logGroupName: ECSFargateRuntime.LOG_GROUP,
|
|
479
|
+
logStreamNamePrefix: `${AWS_CONSTANTS.agentFamily(agentName)}/`,
|
|
480
|
+
limit,
|
|
481
|
+
}));
|
|
482
|
+
return (res.events ?? [])
|
|
483
|
+
.map((e) => e.message?.trimEnd() ?? "")
|
|
484
|
+
.filter(Boolean);
|
|
485
|
+
}
|
|
486
|
+
// --- Internal: Secret naming ---
|
|
487
|
+
awsSecretName(type, instance, field) {
|
|
488
|
+
return `${this.prefix}/${type}/${instance}/${field}`;
|
|
489
|
+
}
|
|
490
|
+
async listSecretFields(type, instance) {
|
|
491
|
+
const prefix = `${this.prefix}/${type}/${instance}/`;
|
|
492
|
+
const res = await this.smClient.send(new ListSecretsCommand({
|
|
493
|
+
Filters: [{ Key: "name", Values: [prefix] }],
|
|
494
|
+
MaxResults: 100,
|
|
495
|
+
}));
|
|
496
|
+
const fields = [];
|
|
497
|
+
for (const secret of res.SecretList || []) {
|
|
498
|
+
if (secret.Name?.startsWith(prefix)) {
|
|
499
|
+
fields.push(secret.Name.slice(prefix.length));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return fields;
|
|
503
|
+
}
|
|
504
|
+
// --- Internal: ECS operations ---
|
|
505
|
+
async registerTaskDefinition(family, opts) {
|
|
506
|
+
const environment = Object.entries(opts.env).map(([name, value]) => ({ name, value }));
|
|
507
|
+
const secrets = opts.secretMounts.map((mount) => {
|
|
508
|
+
const parts = mount.mountPath.replace("/credentials/", "").split("/");
|
|
509
|
+
const envName = `AL_SECRET_${parts.join("__")}`;
|
|
510
|
+
return {
|
|
511
|
+
name: envName,
|
|
512
|
+
valueFrom: `arn:aws:secretsmanager:${this.config.awsRegion}:${this.getAccountId()}:secret:${mount.secretId}`,
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
const res = await this.ecsClient.send(new RegisterTaskDefinitionCommand({
|
|
516
|
+
family,
|
|
517
|
+
networkMode: "awsvpc",
|
|
518
|
+
requiresCompatibilities: ["FARGATE"],
|
|
519
|
+
cpu: opts.cpus,
|
|
520
|
+
memory: opts.memory,
|
|
521
|
+
executionRoleArn: this.config.executionRoleArn,
|
|
522
|
+
taskRoleArn: opts.taskRoleArn,
|
|
523
|
+
containerDefinitions: [{
|
|
524
|
+
name: "agent",
|
|
525
|
+
image: opts.image,
|
|
526
|
+
essential: true,
|
|
527
|
+
environment,
|
|
528
|
+
secrets,
|
|
529
|
+
logConfiguration: {
|
|
530
|
+
logDriver: "awslogs",
|
|
531
|
+
options: {
|
|
532
|
+
"awslogs-group": ECSFargateRuntime.LOG_GROUP,
|
|
533
|
+
"awslogs-region": this.config.awsRegion,
|
|
534
|
+
"awslogs-stream-prefix": opts.streamPrefix,
|
|
535
|
+
"awslogs-create-group": "true",
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
user: "1000:1000",
|
|
539
|
+
linuxParameters: {
|
|
540
|
+
initProcessEnabled: true,
|
|
541
|
+
},
|
|
542
|
+
}],
|
|
543
|
+
}));
|
|
544
|
+
return res.taskDefinition.taskDefinitionArn;
|
|
545
|
+
}
|
|
546
|
+
async runTask(taskDefinitionArn) {
|
|
547
|
+
const res = await this.ecsClient.send(new RunTaskCommand({
|
|
548
|
+
cluster: this.config.ecsCluster,
|
|
549
|
+
taskDefinition: taskDefinitionArn,
|
|
550
|
+
launchType: "FARGATE",
|
|
551
|
+
startedBy: ECSFargateRuntime.STARTED_BY_PREFIX,
|
|
552
|
+
count: 1,
|
|
553
|
+
networkConfiguration: {
|
|
554
|
+
awsvpcConfiguration: {
|
|
555
|
+
subnets: this.config.subnets,
|
|
556
|
+
securityGroups: this.config.securityGroups || [],
|
|
557
|
+
assignPublicIp: "ENABLED",
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
}));
|
|
561
|
+
const tasks = res.tasks || [];
|
|
562
|
+
if (tasks.length === 0) {
|
|
563
|
+
const failures = res.failures || [];
|
|
564
|
+
const reason = failures[0]?.reason || "unknown";
|
|
565
|
+
throw new Error(`Failed to start ECS task: ${reason}`);
|
|
566
|
+
}
|
|
567
|
+
return tasks[0].taskArn;
|
|
568
|
+
}
|
|
569
|
+
async getLogEvents(logGroup, logStream, nextToken) {
|
|
570
|
+
const res = await this.logsClient.send(new GetLogEventsCommand({
|
|
571
|
+
logGroupName: logGroup,
|
|
572
|
+
logStreamName: logStream,
|
|
573
|
+
startFromHead: true,
|
|
574
|
+
nextToken,
|
|
575
|
+
}));
|
|
576
|
+
return {
|
|
577
|
+
events: (res.events || []).map((e) => ({ message: e.message?.trimEnd() || "" })),
|
|
578
|
+
nextForwardToken: res.nextForwardToken,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
// --- Internal: Per-agent task role derivation ---
|
|
582
|
+
deriveTaskRoleArn(agentName) {
|
|
583
|
+
const accountId = this.getAccountId();
|
|
584
|
+
if (!accountId)
|
|
585
|
+
return undefined;
|
|
586
|
+
return `arn:aws:iam::${accountId}:role/${AWS_CONSTANTS.taskRoleName(agentName)}`;
|
|
587
|
+
}
|
|
588
|
+
getAccountId() {
|
|
589
|
+
const match = this.config.ecrRepository.match(/^(\d+)\.dkr\.ecr\./);
|
|
590
|
+
return match?.[1] || "";
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function sleep(ms) {
|
|
594
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
595
|
+
}
|
|
596
|
+
//# sourceMappingURL=ecs-runtime.js.map
|