@ai-outfitter/outfitter 1.3.1 → 1.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.
|
@@ -4,6 +4,23 @@ export interface AgentProcessLauncher {
|
|
|
4
4
|
}
|
|
5
5
|
export declare const launchAgentProcess: (launcher: AgentProcessLauncher, launchPlan: AgentLaunchPlan, agentId: string) => Promise<number>;
|
|
6
6
|
export declare const resolveAgentLaunchExecutable: (launchPlan: AgentLaunchPlan) => AgentLaunchPlan;
|
|
7
|
+
/**
|
|
8
|
+
* Forwards termination signals from this process to a spawned harness, resolving once the harness
|
|
9
|
+
* actually exits.
|
|
10
|
+
*
|
|
11
|
+
* Without this, a resident agent cannot shut down. Node installs no default forwarding, so the
|
|
12
|
+
* harness never learns the session is ending: under Kubernetes it is SIGKILLed when the grace period
|
|
13
|
+
* expires, skipping credential persistence and projection cleanup. The same gap shows up outside
|
|
14
|
+
* containers — Ctrl-C in a terminal, or a cancelled CI job — which is why this belongs here rather
|
|
15
|
+
* than in a container init.
|
|
16
|
+
*
|
|
17
|
+
* Installing a handler suppresses Node's default termination, so every path must resolve, and the
|
|
18
|
+
* listeners must come off afterwards or repeated launches in one process leak them.
|
|
19
|
+
*/
|
|
20
|
+
export declare const attachSignalForwarding: (child: {
|
|
21
|
+
kill(signal?: NodeJS.Signals): boolean;
|
|
22
|
+
killed: boolean;
|
|
23
|
+
}, emitter?: NodeJS.EventEmitter, graceMs?: number) => (() => void);
|
|
7
24
|
export declare const spawnLauncher: AgentProcessLauncher;
|
|
8
25
|
/**
|
|
9
26
|
* Launches a resolved plan through the given spawn boundary. The install-hint agentId is derived
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Turns a logical agent launch plan into an actual launched process: resolves the bundled pi
|
|
2
2
|
// binary, runs the launcher, and translates a missing agent CLI into actionable install guidance.
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
4
5
|
import { dirname, join } from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
export const launchAgentProcess = async (launcher, launchPlan, agentId) => {
|
|
@@ -39,14 +40,64 @@ export const resolveAgentLaunchExecutable = (launchPlan) => {
|
|
|
39
40
|
env: { PI_SKIP_VERSION_CHECK: '1', ...launchPlan.env },
|
|
40
41
|
};
|
|
41
42
|
};
|
|
43
|
+
// Signals we forward to the harness. SIGKILL is deliberately absent: it cannot be caught, and the
|
|
44
|
+
// kernel delivers it to us directly.
|
|
45
|
+
const FORWARDED_SIGNALS = ['SIGTERM', 'SIGINT', 'SIGHUP'];
|
|
46
|
+
// How long the harness gets to exit after a forwarded signal before we stop being polite. Kubernetes
|
|
47
|
+
// defaults to a 30s grace period and SIGKILLs the pod afterwards, so this has to be comfortably
|
|
48
|
+
// shorter or the escalation never runs.
|
|
49
|
+
const TERMINATION_GRACE_MS = 10_000;
|
|
50
|
+
/**
|
|
51
|
+
* Forwards termination signals from this process to a spawned harness, resolving once the harness
|
|
52
|
+
* actually exits.
|
|
53
|
+
*
|
|
54
|
+
* Without this, a resident agent cannot shut down. Node installs no default forwarding, so the
|
|
55
|
+
* harness never learns the session is ending: under Kubernetes it is SIGKILLed when the grace period
|
|
56
|
+
* expires, skipping credential persistence and projection cleanup. The same gap shows up outside
|
|
57
|
+
* containers — Ctrl-C in a terminal, or a cancelled CI job — which is why this belongs here rather
|
|
58
|
+
* than in a container init.
|
|
59
|
+
*
|
|
60
|
+
* Installing a handler suppresses Node's default termination, so every path must resolve, and the
|
|
61
|
+
* listeners must come off afterwards or repeated launches in one process leak them.
|
|
62
|
+
*/
|
|
63
|
+
export const attachSignalForwarding = (child, emitter = process, graceMs = TERMINATION_GRACE_MS) => {
|
|
64
|
+
let escalation;
|
|
65
|
+
const forward = (signal) => () => {
|
|
66
|
+
if (child.killed)
|
|
67
|
+
return;
|
|
68
|
+
child.kill(signal);
|
|
69
|
+
// A harness that ignores or hangs on the signal would otherwise keep us alive until the
|
|
70
|
+
// orchestrator's own SIGKILL, losing the chance to exit cleanly first.
|
|
71
|
+
escalation ??= setTimeout(() => child.kill('SIGKILL'), graceMs);
|
|
72
|
+
escalation.unref?.();
|
|
73
|
+
};
|
|
74
|
+
const handlers = FORWARDED_SIGNALS.map((signal) => [signal, forward(signal)]);
|
|
75
|
+
for (const [signal, handler] of handlers)
|
|
76
|
+
emitter.on(signal, handler);
|
|
77
|
+
return () => {
|
|
78
|
+
for (const [signal, handler] of handlers)
|
|
79
|
+
emitter.removeListener(signal, handler);
|
|
80
|
+
if (escalation)
|
|
81
|
+
clearTimeout(escalation);
|
|
82
|
+
};
|
|
83
|
+
};
|
|
42
84
|
/* v8 ignore start -- real process spawn is covered by end-to-end smoke usage, not unit tests. */
|
|
43
85
|
export const spawnLauncher = {
|
|
44
86
|
async launch(plan) {
|
|
45
87
|
const { default: spawn } = await import('cross-spawn');
|
|
46
88
|
return await new Promise((resolve, reject) => {
|
|
47
89
|
const child = spawn(plan.command, [...plan.args], { stdio: 'inherit', env: { ...process.env, ...plan.env } });
|
|
48
|
-
child
|
|
49
|
-
child.on('
|
|
90
|
+
const detach = attachSignalForwarding(child);
|
|
91
|
+
child.on('error', (error) => {
|
|
92
|
+
detach();
|
|
93
|
+
reject(error); // ENOENT surfaces as an actionable install message
|
|
94
|
+
});
|
|
95
|
+
child.on('close', (code, signal) => {
|
|
96
|
+
detach();
|
|
97
|
+
// 128+n is the shell convention for "died on signal n", and it is what a caller inspecting
|
|
98
|
+
// our exit status expects to see when the harness was terminated rather than returning.
|
|
99
|
+
resolve(code ?? (signal ? 128 + (os.constants.signals[signal] ?? 0) : 0));
|
|
100
|
+
});
|
|
50
101
|
});
|
|
51
102
|
},
|
|
52
103
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentLaunch.js","sourceRoot":"","sources":["../../src/agents/AgentLaunch.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,kGAAkG;AAClG,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAQzC,MAAM,CAAC,MAAM,kBAAkB,GAAG,KAAK,EACrC,QAA8B,EAC9B,UAA2B,EAC3B,OAAe,EACE,EAAE;IACnB,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC3C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,sBAAsB,CAAC,KAAK,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC/F,CAAC;QAED,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,+FAA+F;AAC/F,kGAAkG;AAClG,mGAAmG;AACnG,+FAA+F;AAC/F,yBAAyB;AACzB,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,UAA2B,EAAmB,EAAE;IAC3F,IAAI,UAAU,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QAChC,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,MAAM,eAAe,GAAG,sBAAsB,EAAE,CAAC;IAEjD,oGAAoG;IACpG,IAAI,eAAe,KAAK,SAAS,EAAE,CAAC;QAClC,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,OAAO;QACL,GAAG,UAAU;QACb,OAAO,EAAE,eAAe,CAAC,OAAO;QAChC,IAAI,EAAE,CAAC,GAAG,eAAe,CAAC,UAAU,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC;QACzD,8FAA8F;QAC9F,gGAAgG;QAChG,gGAAgG;QAChG,4FAA4F;QAC5F,GAAG,EAAE,EAAE,qBAAqB,EAAE,GAAG,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE;KACvD,CAAC;AACJ,CAAC,CAAC;AAEF,iGAAiG;AACjG,MAAM,CAAC,MAAM,aAAa,GAAyB;IACjD,KAAK,CAAC,MAAM,CAAC,IAAqB;QAChC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;QACvD,OAAO,MAAM,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACnD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAC9G,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,mDAAmD;
|
|
1
|
+
{"version":3,"file":"AgentLaunch.js","sourceRoot":"","sources":["../../src/agents/AgentLaunch.ts"],"names":[],"mappings":"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"}
|
|
@@ -33,6 +33,7 @@ Outfitter lays out conventions for iterating on and sharing agent configuration
|
|
|
33
33
|
The same composition runs on every surface; only the trigger changes.
|
|
34
34
|
|
|
35
35
|
- [Running an agent in GitHub Actions](./actions.md) — headless runs on any workflow trigger.
|
|
36
|
+
- [Container images](./containers.md) — run the published image persistently or add Nix-packaged tools.
|
|
36
37
|
- [Recurring runs](./recurring-runs.md) — loops three ways: the local loop extension, Actions cron, cluster schedules.
|
|
37
38
|
- [In-cluster agents](./in-cluster.md) — resident agents, CronJobs, and subagent Jobs via Link Operator.
|
|
38
39
|
- [Hooks](./hooks.md) — harness hook wiring and the protocol gap.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Container images
|
|
2
|
+
|
|
3
|
+
The published image is a generic Outfitter runtime:
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
ghcr.io/ai-outfitter/outfitter:<version>
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
It uses `outfitter` as its entrypoint and includes the Nix CLI, Bash, core
|
|
10
|
+
utilities, Git, SSH, and CA certificates. It does not include agent profiles,
|
|
11
|
+
credentials, channels, MCP servers, or other use-case behavior. The default
|
|
12
|
+
runtime user and group are both `1000`, with `/tmp` as the home directory.
|
|
13
|
+
|
|
14
|
+
## Run a resident agent
|
|
15
|
+
|
|
16
|
+
A resident container is an ordinary `outfitter run` whose harness stays in RPC
|
|
17
|
+
mode. Keep standard input open so the harness remains available while its
|
|
18
|
+
extensions wait for work:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
apiVersion: apps/v1
|
|
22
|
+
kind: Deployment
|
|
23
|
+
metadata:
|
|
24
|
+
name: example-agent
|
|
25
|
+
spec:
|
|
26
|
+
replicas: 1
|
|
27
|
+
selector:
|
|
28
|
+
matchLabels:
|
|
29
|
+
app: example-agent
|
|
30
|
+
template:
|
|
31
|
+
metadata:
|
|
32
|
+
labels:
|
|
33
|
+
app: example-agent
|
|
34
|
+
spec:
|
|
35
|
+
securityContext:
|
|
36
|
+
# Pod-level, and not optional. HOME points at the mounted volume, and a
|
|
37
|
+
# freshly provisioned volume arrives owned by root — runAsUser does not
|
|
38
|
+
# change that. Extension installs and credential persistence both write
|
|
39
|
+
# below HOME, so without fsGroup the agent fails with EACCES on first
|
|
40
|
+
# write rather than at startup. A volume plugin that ignores fsGroup
|
|
41
|
+
# must be pre-provisioned with UID/GID 1000 ownership instead.
|
|
42
|
+
fsGroup: 1000
|
|
43
|
+
containers:
|
|
44
|
+
- name: agent
|
|
45
|
+
image: ghcr.io/ai-outfitter/outfitter:<version>
|
|
46
|
+
stdin: true
|
|
47
|
+
workingDir: /workspace
|
|
48
|
+
securityContext:
|
|
49
|
+
runAsNonRoot: true
|
|
50
|
+
runAsUser: 1000
|
|
51
|
+
runAsGroup: 1000
|
|
52
|
+
env:
|
|
53
|
+
- name: HOME
|
|
54
|
+
value: /workspace
|
|
55
|
+
args:
|
|
56
|
+
- run
|
|
57
|
+
- example-agent
|
|
58
|
+
- --strict
|
|
59
|
+
- --
|
|
60
|
+
- --mode
|
|
61
|
+
- rpc
|
|
62
|
+
- --no-session
|
|
63
|
+
volumeMounts:
|
|
64
|
+
- name: workspace
|
|
65
|
+
mountPath: /workspace
|
|
66
|
+
volumes:
|
|
67
|
+
- name: workspace
|
|
68
|
+
persistentVolumeClaim:
|
|
69
|
+
claimName: example-agent
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Mount `.agents` settings, credentials, and any channel configuration through
|
|
73
|
+
the workload that owns the container. The image does not interpret Kubernetes
|
|
74
|
+
resources or impose image, profile, or extension policy.
|
|
75
|
+
|
|
76
|
+
## Build a derivative image
|
|
77
|
+
|
|
78
|
+
Being built with Nix does not normally imply that an image contains the Nix
|
|
79
|
+
CLI. The published Outfitter image includes it intentionally, and the flake also
|
|
80
|
+
exports `lib.mkContainer` for reproducible derivative images:
|
|
81
|
+
|
|
82
|
+
```nix
|
|
83
|
+
{
|
|
84
|
+
inputs = {
|
|
85
|
+
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
86
|
+
outfitter = {
|
|
87
|
+
url = "github:ai-outfitter/outfitter/v1.2.0";
|
|
88
|
+
inputs.nixpkgs.follows = "nixpkgs";
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
outputs =
|
|
93
|
+
{ nixpkgs, outfitter, ... }:
|
|
94
|
+
let
|
|
95
|
+
system = "x86_64-linux";
|
|
96
|
+
pkgs = nixpkgs.legacyPackages.${system};
|
|
97
|
+
in
|
|
98
|
+
{
|
|
99
|
+
packages.${system}.default = outfitter.lib.mkContainer {
|
|
100
|
+
inherit pkgs;
|
|
101
|
+
outfitterPackage = outfitter.packages.${system}.outfitter;
|
|
102
|
+
name = "example-agent";
|
|
103
|
+
extraPackages = [
|
|
104
|
+
pkgs.jq
|
|
105
|
+
pkgs.ripgrep
|
|
106
|
+
];
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Build and exercise the exact image:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
nix build
|
|
116
|
+
docker load < result
|
|
117
|
+
docker run --rm example-agent:latest --version
|
|
118
|
+
docker run --rm --entrypoint /bin/sh example-agent:latest \
|
|
119
|
+
-c 'nix --version && jq --version && rg --version'
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Prefer adding known runtime packages through `extraPackages`. The resulting
|
|
123
|
+
image stays reproducible, and it avoids the trap below.
|
|
124
|
+
|
|
125
|
+
**Do not mount an empty volume over `/nix`.** The image _is_ its Nix store: the
|
|
126
|
+
entrypoint is an absolute store path and every binary in `/bin` is a symlink
|
|
127
|
+
into `/nix/store`. Mounting a fresh volume there hides all of it, so the
|
|
128
|
+
container cannot start — it fails before it could initialize the very store you
|
|
129
|
+
mounted the volume to populate.
|
|
130
|
+
|
|
131
|
+
Runtime installation therefore needs one of:
|
|
132
|
+
|
|
133
|
+
- a volume **pre-populated** with the image's closure, seeded from the image
|
|
134
|
+
before the agent starts (an init container copying `/nix` into the volume);
|
|
135
|
+
- an **overlay** whose lower layer is the image's `/nix`, so the closure stays
|
|
136
|
+
visible while writes land in the upper layer; or
|
|
137
|
+
- writable Nix **state** only — `/nix/var` and a per-user profile — leaving the
|
|
138
|
+
store itself as the image shipped it.
|
package/package.json
CHANGED