@ai-outfitter/outfitter 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/code/pi-extension/src/outfitter-extension.js +9 -1
- package/dist/agents/AgentLaunch.d.ts +2 -2
- package/dist/agents/AgentLaunch.js +4 -3
- package/dist/agents/AgentLaunch.js.map +1 -1
- package/dist/agents/ClaudeStatePersistence.d.ts +18 -0
- package/dist/agents/ClaudeStatePersistence.js +225 -0
- package/dist/agents/ClaudeStatePersistence.js.map +1 -0
- package/dist/cli/OutfitterCli.js +1 -1
- package/dist/cli/OutfitterCli.js.map +1 -1
- package/dist/cli/commands/RunAgentCommand.js +76 -15
- package/dist/cli/commands/RunAgentCommand.js.map +1 -1
- package/dist/cli/commands/SyncCommand.d.ts +7 -3
- package/dist/cli/commands/SyncCommand.js +110 -20
- package/dist/cli/commands/SyncCommand.js.map +1 -1
- package/dist/extensions/PiExtensionCache.d.ts +11 -1
- package/dist/extensions/PiExtensionCache.js +129 -13
- package/dist/extensions/PiExtensionCache.js.map +1 -1
- package/dist/projection/CodexMcp.d.ts +11 -0
- package/dist/projection/CodexMcp.js +143 -0
- package/dist/projection/CodexMcp.js.map +1 -0
- package/dist/projection/Materialize.d.ts +3 -1
- package/dist/projection/Materialize.js +10 -7
- package/dist/projection/Materialize.js.map +1 -1
- package/dist/projection/ProjectHarness.js +75 -32
- package/dist/projection/ProjectHarness.js.map +1 -1
- package/dist/projection/Projection.d.ts +2 -0
- package/dist/projection/Tools.d.ts +91 -0
- package/dist/projection/Tools.js +129 -0
- package/dist/projection/Tools.js.map +1 -0
- package/dist/resolver/AgentDefinition.d.ts +11 -0
- package/dist/resolver/AgentDefinition.js +91 -12
- package/dist/resolver/AgentDefinition.js.map +1 -1
- package/dist/resolver/Layer.d.ts +5 -2
- package/dist/resolver/Layer.js +47 -8
- package/dist/resolver/Layer.js.map +1 -1
- package/dist/resolver/ResolverContext.d.ts +1 -1
- package/dist/resolver/ResolverContext.js +1 -1
- package/dist/resolver/ResolverContext.js.map +1 -1
- package/dist/resolver/ResolverValidation.d.ts +12 -1
- package/dist/resolver/ResolverValidation.js +30 -20
- package/dist/resolver/ResolverValidation.js.map +1 -1
- package/dist/schemas/agent.schema.json +8 -2
- package/dist/schemas/settings.schema.json +1 -1
- package/dist/schemas/system-extension-hook.schema.json +38 -0
- package/dist/settings/Settings.d.ts +1 -1
- package/dist/settings/Settings.js +1 -1
- package/dist/settings/Settings.js.map +1 -1
- package/dist/settings/SettingsLoader.js.map +1 -1
- package/dist/setup/DefaultCatalog.d.ts +18 -0
- package/dist/setup/DefaultCatalog.js +74 -13
- package/dist/setup/DefaultCatalog.js.map +1 -1
- package/dist/sources/SourceCache.d.ts +20 -0
- package/dist/sources/SourceCache.js +31 -1
- package/dist/sources/SourceCache.js.map +1 -1
- package/dist/sources/TransitiveSources.d.ts +43 -0
- package/dist/sources/TransitiveSources.js +177 -0
- package/dist/sources/TransitiveSources.js.map +1 -0
- package/dist/system/SystemExtensionHook.d.ts +34 -0
- package/dist/system/SystemExtensionHook.js +190 -0
- package/dist/system/SystemExtensionHook.js.map +1 -0
- package/dist/validation/SchemaValidator.d.ts +1 -1
- package/dist/validation/SchemaValidator.js +2 -0
- package/dist/validation/SchemaValidator.js.map +1 -1
- package/docs/architecture/state_writeback_strategy.md +15 -3
- package/docs/documentation/README.md +15 -2
- package/docs/documentation/catalogs.md +61 -2
- package/docs/documentation/cli.md +2 -1
- package/docs/documentation/concepts.md +2 -2
- package/docs/documentation/containers.md +63 -14
- package/docs/documentation/hooks.md +33 -0
- package/docs/documentation/in-cluster.md +2 -0
- package/docs/documentation/migration.md +1 -1
- package/docs/documentation/personas.md +1 -1
- package/docs/documentation/settings.md +1 -1
- package/docs/documentation/state.md +27 -0
- package/docs/documentation/support-matrix.md +36 -23
- package/docs/documentation/usecases/org-onboarding-sdlc-report.md +143 -0
- package/docs/documentation/usecases/persona-reviews.md +1 -1
- package/docs/philosophy.md +20 -0
- package/package.json +1 -1
- package/src/schemas/agent.schema.json +8 -2
- package/src/schemas/settings.schema.json +1 -1
- package/src/schemas/system-extension-hook.schema.json +38 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AgentLaunchPlan } from '../projection/Projection.js';
|
|
2
|
+
import type { Harness } from '../settings/Settings.js';
|
|
3
|
+
export interface SystemExtensionHarnessHook {
|
|
4
|
+
readonly extensions?: readonly string[];
|
|
5
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
6
|
+
}
|
|
7
|
+
export interface SystemExtensionHook {
|
|
8
|
+
readonly filePath: string;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly harnesses: Partial<Readonly<Record<Harness, SystemExtensionHarnessHook>>>;
|
|
11
|
+
}
|
|
12
|
+
export interface SystemExtensionHookSource {
|
|
13
|
+
readonly directory: string;
|
|
14
|
+
readonly stamp: string;
|
|
15
|
+
}
|
|
16
|
+
export interface LoadedSystemExtensionHooks {
|
|
17
|
+
readonly hooks: readonly SystemExtensionHook[];
|
|
18
|
+
readonly source?: SystemExtensionHookSource;
|
|
19
|
+
}
|
|
20
|
+
export interface AttachedSystemExtensionHooks {
|
|
21
|
+
readonly launch: AgentLaunchPlan;
|
|
22
|
+
readonly warnings: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
export interface SystemExtensionHookDiscoveryInput {
|
|
25
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
26
|
+
readonly platform?: NodeJS.Platform;
|
|
27
|
+
}
|
|
28
|
+
export declare const resolveSystemExtensionHookSource: (input?: SystemExtensionHookDiscoveryInput) => SystemExtensionHookSource | undefined;
|
|
29
|
+
export declare const readSystemExtensionHooks: (input?: SystemExtensionHookDiscoveryInput) => LoadedSystemExtensionHooks;
|
|
30
|
+
/**
|
|
31
|
+
* Prepends every system Pi extension after projection, including for RPC/print launches. Hook env
|
|
32
|
+
* stays beneath the projected plan env so it cannot replace Outfitter's runtime/session paths.
|
|
33
|
+
*/
|
|
34
|
+
export declare const attachSystemExtensionHooks: (plan: AgentLaunchPlan, loaded?: LoadedSystemExtensionHooks) => AttachedSystemExtensionHooks;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Reads root-owned system extension hooks for post-projection launch-plan mutation. This is a
|
|
2
|
+
// launcher-scope mechanism, not a managed configuration scope: Pi never reads these files, so they
|
|
3
|
+
// do not change Pi's configuration resolution order. A session can still bypass the launcher with
|
|
4
|
+
// the bundled Pi binary directly or point OUTFITTER_SYSTEM_DIR at an empty directory. The honest
|
|
5
|
+
// guarantee is that collection is on by default and the organization owns the file naming the
|
|
6
|
+
// collector, not that a session cannot turn collection off.
|
|
7
|
+
//
|
|
8
|
+
// Hook documents fail closed because only an operator can create files in the normal root-owned
|
|
9
|
+
// locations. A malformed file is therefore an operator error caught on a canary boot; failing open
|
|
10
|
+
// would silently run the fleet without collection, which downstream verifiers must classify as
|
|
11
|
+
// unattested rather than clean. Hook environment also denies Node and native dynamic-loader
|
|
12
|
+
// controls: they could execute code before Pi starts and change what runs rather than what observes.
|
|
13
|
+
// Symlinks are resolved to physical paths and dangling links are fatal. Ownership and mode checks
|
|
14
|
+
// stay with deployment policy because supported paths include platform-specific package stores and
|
|
15
|
+
// the explicit development override, which do not share one portable owner or permission model.
|
|
16
|
+
import { lstatSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
17
|
+
import { isAbsolute, join } from 'node:path';
|
|
18
|
+
import { validateSchema } from '../validation/SchemaValidator.js';
|
|
19
|
+
import { parseYamlDocument } from '../validation/YamlDocument.js';
|
|
20
|
+
const linuxSystemDirectory = '/etc/outfitter/system.d';
|
|
21
|
+
const macosSystemDirectory = '/Library/Application Support/Outfitter/system.d';
|
|
22
|
+
const protectedPiEnvironmentVariables = new Set(['PI_CODING_AGENT_DIR', 'PI_CODING_AGENT_SESSION_DIR']);
|
|
23
|
+
const runtimeControlEnvironmentVariables = new Set([
|
|
24
|
+
'LD_AUDIT',
|
|
25
|
+
'LD_LIBRARY_PATH',
|
|
26
|
+
'LD_PRELOAD',
|
|
27
|
+
'NODE_OPTIONS',
|
|
28
|
+
'NODE_REPL_EXTERNAL_MODULE',
|
|
29
|
+
// Node reads this before startup and OpenSSL configuration can activate an
|
|
30
|
+
// arbitrary native provider, so it executes code exactly like the loaders above.
|
|
31
|
+
'OPENSSL_CONF',
|
|
32
|
+
]);
|
|
33
|
+
export const resolveSystemExtensionHookSource = (input = {}) => {
|
|
34
|
+
const environment = input.environment ?? process.env;
|
|
35
|
+
const override = environment.OUTFITTER_SYSTEM_DIR;
|
|
36
|
+
if (override !== undefined)
|
|
37
|
+
return { directory: override, stamp: `env-override:${override}` };
|
|
38
|
+
switch (input.platform ?? process.platform) {
|
|
39
|
+
case 'darwin':
|
|
40
|
+
return { directory: macosSystemDirectory, stamp: macosSystemDirectory };
|
|
41
|
+
case 'linux':
|
|
42
|
+
return { directory: linuxSystemDirectory, stamp: linuxSystemDirectory };
|
|
43
|
+
default:
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const formatValidationIssues = (filePath, issues) => issues.map((issue) => `${filePath}#${issue.path} ${issue.message}`).join('; ');
|
|
48
|
+
const resolvePhysicalPath = (path, errorMessage) => {
|
|
49
|
+
try {
|
|
50
|
+
return realpathSync(path);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw new Error(`${errorMessage}: ${String(error)}`, { cause: error });
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const resolveExtensionPaths = (document, filePath) => {
|
|
57
|
+
const harnesses = {};
|
|
58
|
+
for (const [harness, hook] of Object.entries(document.harnesses)) {
|
|
59
|
+
if (hook.extensions === undefined) {
|
|
60
|
+
harnesses[harness] = hook;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
harnesses[harness] = {
|
|
64
|
+
...hook,
|
|
65
|
+
extensions: hook.extensions.map((extensionPath) => {
|
|
66
|
+
if (!isAbsolute(extensionPath) || lstatSync(extensionPath, { throwIfNoEntry: false }) === undefined) {
|
|
67
|
+
throw new Error(`Invalid system extension hook '${filePath}': extension path does not exist: ${extensionPath}`);
|
|
68
|
+
}
|
|
69
|
+
return resolvePhysicalPath(extensionPath, `Invalid system extension hook '${filePath}': extension path cannot be resolved: ${extensionPath}`);
|
|
70
|
+
}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return { ...document, harnesses };
|
|
74
|
+
};
|
|
75
|
+
const assertNoProtectedPiEnvironment = (document, filePath) => {
|
|
76
|
+
for (const hook of Object.values(document.harnesses)) {
|
|
77
|
+
for (const name of Object.keys(hook.env ?? {})) {
|
|
78
|
+
if (protectedPiEnvironmentVariables.has(name.toUpperCase())) {
|
|
79
|
+
throw new Error(`Invalid system extension hook '${filePath}': environment variable '${name}' is reserved by Outfitter.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const assertNoRuntimeControlEnvironment = (document, filePath) => {
|
|
85
|
+
for (const hook of Object.values(document.harnesses)) {
|
|
86
|
+
for (const name of Object.keys(hook.env ?? {})) {
|
|
87
|
+
const normalizedName = name.toUpperCase();
|
|
88
|
+
if (runtimeControlEnvironmentVariables.has(normalizedName) || normalizedName.startsWith('DYLD_')) {
|
|
89
|
+
throw new Error(`Invalid system extension hook '${filePath}': environment variable '${name}' controls process loading and is forbidden.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const readSystemExtensionHook = (filePath) => {
|
|
95
|
+
if (lstatSync(filePath, { throwIfNoEntry: false }) === undefined) {
|
|
96
|
+
/* v8 ignore next -- the path was returned by the immediately preceding readdirSync call. */
|
|
97
|
+
throw new Error(`System extension hook '${filePath}' disappeared while loading.`);
|
|
98
|
+
}
|
|
99
|
+
const physicalFilePath = resolvePhysicalPath(filePath, `System extension hook '${filePath}' cannot be resolved`);
|
|
100
|
+
let content;
|
|
101
|
+
try {
|
|
102
|
+
content = readFileSync(physicalFilePath, 'utf8');
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw new Error(`System extension hook '${physicalFilePath}' is unreadable: ${String(error)}`, { cause: error });
|
|
106
|
+
}
|
|
107
|
+
const parsed = parseYamlDocument(content, physicalFilePath);
|
|
108
|
+
if (!parsed.ok) {
|
|
109
|
+
throw new Error(`Invalid system extension hook '${physicalFilePath}': ${parsed.issue.message}`);
|
|
110
|
+
}
|
|
111
|
+
const validation = validateSchema('system-extension-hook', parsed.document);
|
|
112
|
+
if (!validation.valid) {
|
|
113
|
+
throw new Error(`Invalid system extension hook: ${formatValidationIssues(physicalFilePath, validation.issues)}`);
|
|
114
|
+
}
|
|
115
|
+
const document = parsed.document;
|
|
116
|
+
assertNoProtectedPiEnvironment(document, physicalFilePath);
|
|
117
|
+
assertNoRuntimeControlEnvironment(document, physicalFilePath);
|
|
118
|
+
return { filePath: physicalFilePath, ...resolveExtensionPaths(document, physicalFilePath) };
|
|
119
|
+
};
|
|
120
|
+
const assertNoEnvironmentCollisions = (hooks) => {
|
|
121
|
+
const owners = new Map();
|
|
122
|
+
for (const hook of hooks) {
|
|
123
|
+
for (const harness of ['pi', 'claude', 'codex']) {
|
|
124
|
+
for (const name of Object.keys(hook.harnesses[harness]?.env ?? {})) {
|
|
125
|
+
const key = `${harness}:${name}`;
|
|
126
|
+
const owner = owners.get(key);
|
|
127
|
+
if (owner !== undefined) {
|
|
128
|
+
throw new Error(`Invalid system extension hooks: environment '${name}' for '${harness}' is declared by both '${owner}' and '${hook.filePath}'.`);
|
|
129
|
+
}
|
|
130
|
+
owners.set(key, hook.filePath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
export const readSystemExtensionHooks = (input = {}) => {
|
|
136
|
+
const source = resolveSystemExtensionHookSource(input);
|
|
137
|
+
if (source === undefined)
|
|
138
|
+
return { hooks: [] };
|
|
139
|
+
const sourceEntry = lstatSync(source.directory, { throwIfNoEntry: false });
|
|
140
|
+
if (sourceEntry === undefined)
|
|
141
|
+
return { hooks: [], source };
|
|
142
|
+
const physicalDirectory = resolvePhysicalPath(source.directory, `System extension hook source '${source.directory}' cannot be resolved`);
|
|
143
|
+
const sourceStat = statSync(physicalDirectory);
|
|
144
|
+
if (!sourceStat.isDirectory()) {
|
|
145
|
+
throw new Error(`System extension hook source '${physicalDirectory}' is not a directory.`);
|
|
146
|
+
}
|
|
147
|
+
const physicalSource = {
|
|
148
|
+
directory: physicalDirectory,
|
|
149
|
+
stamp: `${source.stamp.slice(0, source.stamp.length - source.directory.length)}${physicalDirectory}`,
|
|
150
|
+
};
|
|
151
|
+
const filePaths = readdirSync(physicalDirectory)
|
|
152
|
+
.filter((name) => name.endsWith('.yml'))
|
|
153
|
+
.sort()
|
|
154
|
+
.map((name) => join(physicalDirectory, name));
|
|
155
|
+
const hooks = filePaths.map(readSystemExtensionHook);
|
|
156
|
+
assertNoEnvironmentCollisions(hooks);
|
|
157
|
+
return { hooks, source: physicalSource };
|
|
158
|
+
};
|
|
159
|
+
const unsupportedHarnessWarnings = (hooks) => hooks.flatMap((hook) => ['claude', 'codex']
|
|
160
|
+
.filter((harness) => hook.harnesses[harness] !== undefined)
|
|
161
|
+
.map((harness) => `warning: System extension hook '${hook.name}' configures unsupported harness '${harness}'; ignoring it.`));
|
|
162
|
+
/**
|
|
163
|
+
* Prepends every system Pi extension after projection, including for RPC/print launches. Hook env
|
|
164
|
+
* stays beneath the projected plan env so it cannot replace Outfitter's runtime/session paths.
|
|
165
|
+
*/
|
|
166
|
+
export const attachSystemExtensionHooks = (plan, loaded = readSystemExtensionHooks()) => {
|
|
167
|
+
const warnings = unsupportedHarnessWarnings(loaded.hooks);
|
|
168
|
+
if (loaded.source === undefined)
|
|
169
|
+
return { launch: plan, warnings };
|
|
170
|
+
const piHooks = loaded.hooks.flatMap((hook) => (hook.harnesses.pi === undefined ? [] : [hook.harnesses.pi]));
|
|
171
|
+
const extensionArgs = plan.command === 'pi'
|
|
172
|
+
? piHooks.flatMap((hook) => (hook.extensions ?? []).flatMap((path) => ['--extension', path]))
|
|
173
|
+
: [];
|
|
174
|
+
const hookEnvironment = plan.command === 'pi'
|
|
175
|
+
? piHooks.reduce((environment, hook) => ({ ...environment, ...hook.env }), {})
|
|
176
|
+
: {};
|
|
177
|
+
return {
|
|
178
|
+
launch: {
|
|
179
|
+
...plan,
|
|
180
|
+
args: [...extensionArgs, ...plan.args],
|
|
181
|
+
env: {
|
|
182
|
+
...hookEnvironment,
|
|
183
|
+
OUTFITTER_SYSTEM_HOOK_SOURCE: loaded.source.stamp,
|
|
184
|
+
...plan.env,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
warnings,
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
//# sourceMappingURL=SystemExtensionHook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SystemExtensionHook.js","sourceRoot":"","sources":["../../src/system/SystemExtensionHook.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,10 +4,12 @@ import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
|
4
4
|
const readSchema = (schemaFileName) => JSON.parse(readFileSync(new URL(`../schemas/${schemaFileName}`, import.meta.url), 'utf8'));
|
|
5
5
|
const settingsSchema = readSchema('settings.schema.json');
|
|
6
6
|
const agentSchema = readSchema('agent.schema.json');
|
|
7
|
+
const systemExtensionHookSchema = readSchema('system-extension-hook.schema.json');
|
|
7
8
|
const ajv = new Ajv2020({ allErrors: true });
|
|
8
9
|
const validators = {
|
|
9
10
|
settings: ajv.compile(settingsSchema),
|
|
10
11
|
agent: ajv.compile(agentSchema),
|
|
12
|
+
'system-extension-hook': ajv.compile(systemExtensionHookSchema),
|
|
11
13
|
};
|
|
12
14
|
export const createValidationResult = (issues) => ({
|
|
13
15
|
valid: issues.length === 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SchemaValidator.js","sourceRoot":"","sources":["../../src/validation/SchemaValidator.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGvC,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAc3C,MAAM,UAAU,GAAG,CAAC,cAAsB,EAAW,EAAE,CACrD,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,cAAc,cAAc,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAE7F,MAAM,cAAc,GAAG,UAAU,CAAC,sBAAsB,CAAC,CAAC;AAC1D,MAAM,WAAW,GAAG,UAAU,CAAC,mBAAmB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"SchemaValidator.js","sourceRoot":"","sources":["../../src/validation/SchemaValidator.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAGvC,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAc3C,MAAM,UAAU,GAAG,CAAC,cAAsB,EAAW,EAAE,CACrD,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,cAAc,cAAc,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;AAE7F,MAAM,cAAc,GAAG,UAAU,CAAC,sBAAsB,CAAC,CAAC;AAC1D,MAAM,WAAW,GAAG,UAAU,CAAC,mBAAmB,CAAC,CAAC;AACpD,MAAM,yBAAyB,GAAG,UAAU,CAAC,mCAAmC,CAAC,CAAC;AAElF,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAE7C,MAAM,UAAU,GAAyC;IACvD,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC,cAA2B,CAAC;IAClD,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,WAAwB,CAAC;IAC5C,uBAAuB,EAAE,GAAG,CAAC,OAAO,CAAC,yBAAsC,CAAC;CAC7E,CAAC;AAEF,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,MAAkC,EAAoB,EAAE,CAAC,CAAC;IAC/F,KAAK,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;IAC1B,MAAM;CACP,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,UAAsB,EAAE,QAAiB,EAAoB,EAAE;IAC5F,MAAM,QAAQ,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IAExC,IAAI,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACvB,OAAO,sBAAsB,CAAC,EAAE,CAAC,CAAC;IACpC,CAAC;IAED,OAAO,sBAAsB,CAAE,QAAQ,CAAC,MAAiC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC;AACjG,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,CAAC,KAAkB,EAAmB,EAAE,CAAC,CAAC;IAC/D,IAAI,EAAE,KAAK,CAAC,YAAY,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY;IAC1D,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC;CAC/B,CAAC,CAAC"}
|
|
@@ -26,7 +26,19 @@ The user-facing state update lifecycle is:
|
|
|
26
26
|
## Current behavior
|
|
27
27
|
|
|
28
28
|
- Projections remain temporary and reproducible by default.
|
|
29
|
-
- Outfitter does not do generic post-run copy-back or JSON/YAML merge-back.
|
|
29
|
+
- Outfitter does not do generic post-run copy-back or JSON/YAML merge-back. The Pi and Claude
|
|
30
|
+
adapters have narrow credential and session bridges as exceptions because the current launch flow does not
|
|
31
|
+
connect those files to durable storage through materialized declared-state symlinks. Pi declares
|
|
32
|
+
`auth.json` and `models.json` with a `symlink` default, but currently copies them from the durable
|
|
33
|
+
agent directory into the projection before launch and back from the projection afterward. Claude
|
|
34
|
+
similarly copies `.credentials.json` from `~/.claude` into the projection and copies a changed
|
|
35
|
+
file back afterward. Claude's separate `~/.claude.json` sits outside `~/.claude`, so it cannot be
|
|
36
|
+
symlinked with the rest of the config directory; Outfitter seeds a narrow whitelist into the
|
|
37
|
+
projected file and merges only `oauthAccount` back rather than copying machine-local state
|
|
38
|
+
wholesale. Claude's `CLAUDE_CONFIG_DIR` also redirects session transcripts into the projection,
|
|
39
|
+
so Outfitter seeds only the current working directory's `projects/<slug>/` before launch and
|
|
40
|
+
atomically merges projected session files from every slug back afterward. This per-file merge
|
|
41
|
+
never deletes durable history and runs after both normal and thrown launcher exits.
|
|
30
42
|
- Persistent state is represented by symlinking a projection path to the native CLI fallback path.
|
|
31
43
|
- Adapters may generate a concrete runtime file for a declared state path when they need deterministic launch-time reconciliation. For example, the Pi adapter can generate a transformed `settings.json` that removes native `packages` entries already supplied by composition-controlled extensions, and then mark that declared path as `discard` for write detection during the run.
|
|
32
44
|
- Every adapter-declared state path has a resolved strategy before launch: settings overrides win, otherwise the adapter `default_strategy` is used, except for adapter-generated reconciliation files that are intentionally treated as discarded runtime files.
|
|
@@ -181,7 +193,7 @@ Settings may override persistence by mapping adapter-declared paths to strategy
|
|
|
181
193
|
|
|
182
194
|
```yaml
|
|
183
195
|
state_persistence:
|
|
184
|
-
auth.json: symlink
|
|
196
|
+
auth.json: symlink # Intended policy; Pi's current launch flow uses its explicit copy bridge.
|
|
185
197
|
settings.json: symlink
|
|
186
198
|
plugins/: symlink
|
|
187
199
|
cache/: discard
|
|
@@ -198,7 +210,7 @@ Functional examples:
|
|
|
198
210
|
```yaml
|
|
199
211
|
# Persist logins and settings, but make caches and sessions run-local.
|
|
200
212
|
state_persistence:
|
|
201
|
-
auth.json: symlink
|
|
213
|
+
auth.json: symlink # Intended policy; Pi's current launch flow uses its explicit copy bridge.
|
|
202
214
|
settings.json: symlink
|
|
203
215
|
cache/: discard
|
|
204
216
|
sessions/: discard
|
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Outfitter lays out conventions for iterating on and sharing agent configuration — agent profiles, skills, and loadouts. The docs follow that arc: **set up** your own, **understand** the pieces, **share** them across projects and your org, **automate** them on more surfaces, then contribute back. Each page keeps to one concept and links onward when you need the next one, so nothing here has to be read up front.
|
|
4
4
|
|
|
5
|
+
Onboarding an organization? Start with
|
|
6
|
+
[Onboard an organization with an SDLC report](./usecases/org-onboarding-sdlc-report.md) —
|
|
7
|
+
one engineer runs the maturity assessment, then creates the org `.agents`
|
|
8
|
+
repository with the baseline report as its first commit.
|
|
9
|
+
|
|
10
|
+
## Climb the ramp (runbooks)
|
|
11
|
+
|
|
12
|
+
One runbook per rung of [the adoption ramp](../philosophy.md#the-ramp-to-an-autonomous-lifecycle). Each starts where the previous one ended and closes with the one concrete step that begins the next rung. Their success checks are the signals an SDLC assessment reports, so "done" is something you run rather than something you judge.
|
|
13
|
+
|
|
14
|
+
- [Share one catalog](../runbooks/share-one-catalog.md) — one pinned catalog the organization shares, instead of per-laptop configuration. (→ delegated)
|
|
15
|
+
- [Run it without your laptop](../runbooks/run-without-your-laptop.md) — an event triggers the workflow, its output lands through review, and the session is captured. (→ automated)
|
|
16
|
+
- [Give the agent a residence](../runbooks/give-the-agent-a-residence.md) — a named, assignable agent with an account and somewhere to live. (→ governed)
|
|
17
|
+
|
|
5
18
|
## Start (you)
|
|
6
19
|
|
|
7
20
|
- [Getting started](./getting-started.md) — install, first run, default agent.
|
|
@@ -33,10 +46,10 @@ Outfitter lays out conventions for iterating on and sharing agent configuration
|
|
|
33
46
|
The same composition runs on every surface; only the trigger changes.
|
|
34
47
|
|
|
35
48
|
- [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
|
|
49
|
+
- [Container images](./containers.md) — run the published Debian-based image persistently, extend it with apt, or use the `-nix` variant.
|
|
37
50
|
- [Recurring runs](./recurring-runs.md) — loops three ways: the local loop extension, Actions cron, cluster schedules.
|
|
38
51
|
- [In-cluster agents](./in-cluster.md) — resident agents, CronJobs, and subagent Jobs via Link Operator.
|
|
39
|
-
- [Hooks](./hooks.md) — harness hook wiring and the protocol gap.
|
|
52
|
+
- [Hooks](./hooks.md) — harness hook wiring, launcher-scope system observers, and the protocol gap.
|
|
40
53
|
- [Dump](./dump-and-bake.md) — deterministic, self-contained `.agents/` dumps.
|
|
41
54
|
- [State persistence](./state.md)
|
|
42
55
|
- [Adapter support matrix](./support-matrix.md)
|
|
@@ -83,6 +83,43 @@ Remote entries additionally accept:
|
|
|
83
83
|
|
|
84
84
|
Resources from all sources resolve by slug behind local layers, following [layer precedence](./concepts.md#layer-precedence). Agent-local skills keep their owning-agent namespace through cache and source merging. Outfitter reports shadowed IDs so consumers can see which source supplies a selected resource.
|
|
85
85
|
|
|
86
|
+
### Catalog dependencies (transitive sources)
|
|
87
|
+
|
|
88
|
+
A catalog can depend on other catalogs by declaring `sources` in its own settings file
|
|
89
|
+
(`settings.yml` at its payload root, or `.agents/settings.yml`). Outfitter resolves those
|
|
90
|
+
declarations transitively: syncing and resolving a catalog also fetches and layers the catalogs it
|
|
91
|
+
declares, so one pinned root pulls in its dependency closure.
|
|
92
|
+
|
|
93
|
+
Transitive sources are deliberately the narrowest safe subset — a `github:` shorthand pinned to an
|
|
94
|
+
immutable ref — while the [remote-catalog trust model](https://github.com/ai-outfitter/outfitter/issues/212)
|
|
95
|
+
is defined. Anything else a catalog declares is skipped with a warning:
|
|
96
|
+
|
|
97
|
+
- **`github:` shorthand only.** A transitive source must be a `github: owner/repo` shorthand. A
|
|
98
|
+
`uri:` source declared by a catalog is skipped, because a URI can name an arbitrary git transport
|
|
99
|
+
(for example a local path or a remote helper) that a dependency should not be able to choose on
|
|
100
|
+
your behalf. Keeping to `github:` also routes every transitive fetch that `outfitter sync`
|
|
101
|
+
performs through the same private-catalog gate as your own sources. (The one exception is the
|
|
102
|
+
first-party default catalog's own closure, fetched during setup — see the note below.)
|
|
103
|
+
- **Whole repository only.** A transitive `github:` source may not carry a `path:` subpath, so a
|
|
104
|
+
declaration can never point outside the repository it fetches.
|
|
105
|
+
- **Pinned only.** A transitive source must pin an immutable `ref:` — a full commit SHA, or a
|
|
106
|
+
version tag such as `v1.2.0`. A commit SHA is truly immutable; a version tag is a pin the
|
|
107
|
+
dependency's maintainer could later move, in which case the next `outfitter sync` fetches the
|
|
108
|
+
new commit and reports it as `updated`. Pin dependencies you rely on to a SHA when you need the
|
|
109
|
+
closure to never change underneath you.
|
|
110
|
+
- **Content only.** A depended-on catalog contributes `.agents` payload resources. Nothing else in
|
|
111
|
+
its settings file — default agent, default harness, cache directory, `remote_settings` — takes
|
|
112
|
+
effect transitively.
|
|
113
|
+
- **Lower precedence.** Every source you configure directly outranks every transitive source;
|
|
114
|
+
deeper dependencies rank below shallower ones.
|
|
115
|
+
- Cycles and duplicates resolve once — the first occurrence wins and resolution terminates.
|
|
116
|
+
|
|
117
|
+
A fresh `outfitter` install fetches this closure during setup, so a default profile whose skills
|
|
118
|
+
live in a depended-on catalog works without a manual sync. Because the default catalog is the
|
|
119
|
+
first-party catalog Outfitter ships (pinned in the CLI), its bootstrap fetches the declared closure
|
|
120
|
+
directly; the interactive private-catalog prompt is a property of `outfitter sync`, which is where
|
|
121
|
+
you add your own third-party sources.
|
|
122
|
+
|
|
86
123
|
## Organization control repositories
|
|
87
124
|
|
|
88
125
|
An `owner/.outfitter` repository distributes organization-wide resources plus shared settings that Outfitter layers below each user's local settings:
|
|
@@ -104,7 +141,10 @@ Remote settings are cached locally and merged at lower precedence than your proj
|
|
|
104
141
|
1. Local settings are validated. Remote settings repositories are cloned or updated first, then
|
|
105
142
|
merged settings are reloaded.
|
|
106
143
|
2. Remote sources (including any added by remote settings) are cloned or updated.
|
|
107
|
-
3.
|
|
144
|
+
3. Sources declared by the synced catalogs themselves (see
|
|
145
|
+
[catalog dependencies](#catalog-dependencies-transitive-sources)) are fetched next, repeating
|
|
146
|
+
until the whole dependency closure is cached.
|
|
147
|
+
4. Each synced source is validated; sync reports `updated`, `unchanged`, `skipped`, or `failed` per source.
|
|
108
148
|
|
|
109
149
|
All repositories live under `<cache_directory>/repos/<encoded-uri-and-ref>/` (default
|
|
110
150
|
`~/.agents/cache`). Pinned (`ref:`) sources stay on their selected ref until you change it; unpinned
|
|
@@ -119,6 +159,23 @@ synchronization; run sync explicitly when you want network updates.
|
|
|
119
159
|
|
|
120
160
|
Private GitHub catalogs are an enterprise feature. When sync detects a private GitHub repository, it asks for confirmation before use and records the decision in your user settings. Review the Outfitter Enterprise license or your enterprise agreement before enabling private catalogs. Non-GitHub `uri:` sources use whatever git credentials your environment already has; credentials embedded in URIs are redacted from sync output.
|
|
121
161
|
|
|
162
|
+
### How sync authenticates
|
|
163
|
+
|
|
164
|
+
Outfitter does not collect, store, or validate credentials. It delegates to `git`, so a private catalog clones with whatever credentials the surrounding environment already gives `git` — which differs by where sync runs:
|
|
165
|
+
|
|
166
|
+
| Where | Credential |
|
|
167
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
168
|
+
| Your machine | Your existing git configuration: SSH agent, credential helper, or `.netrc`. |
|
|
169
|
+
| GitHub Actions | The workflow token, configured for git — see [token-permissions.md](https://github.com/ai-outfitter/actions/blob/main/docs/token-permissions.md). |
|
|
170
|
+
| A cluster pod | Supplied by the deployment: `GIT_ASKPASS` over HTTPS, or `GIT_SSH_COMMAND` for a deploy key. |
|
|
171
|
+
|
|
172
|
+
Two failure modes are worth knowing before you hit them:
|
|
173
|
+
|
|
174
|
+
- **Credentials belong in the environment, not the URI.** Outfitter redacts credentials from a source URI before deriving its cache path, so a URI carrying a username produces a cache entry that later runs do not read. The source URI must be byte-identical everywhere it appears.
|
|
175
|
+
- **`outfitter run` does not sync.** A runtime that has never synced has an empty cache and cannot resolve a profile from it, however good its credentials are.
|
|
176
|
+
|
|
177
|
+
[The forge credential model](../architecture/forge-credential-model.md) covers which credential to use where, and why.
|
|
178
|
+
|
|
122
179
|
## Trust and review
|
|
123
180
|
|
|
124
181
|
Adding a catalog source means trusting its authors with your agent runtime. A catalog's resources can shape prompts and policy (agents, `agents.md`, `system-prompt.md`), add MCP servers (`mcp.json`), and ship skills whose scripts execute on your machine.
|
|
@@ -129,6 +186,8 @@ Before adding a source, review it:
|
|
|
129
186
|
2. Read every skill you will select, including its scripts and catalog-owned `file` references (see the [trust boundary](./skills.md#trust-boundary)).
|
|
130
187
|
3. Review `mcp.json` — MCP servers are code with whatever access you grant them.
|
|
131
188
|
4. Check `remote_settings` targets: a settings file can add further sources you did not review.
|
|
132
|
-
5.
|
|
189
|
+
5. Check the catalog's own `sources`: its pinned `github:` dependencies are fetched transitively, so
|
|
190
|
+
review each one like the catalog itself.
|
|
191
|
+
6. Confirm the repository's ownership and that its maintainers are who you expect.
|
|
133
192
|
|
|
134
193
|
**Pin a `ref:`** — ideally a full commit SHA — for any catalog you do not maintain yourself, and always for catalogs consumed in CI (see [Running tasks in GitHub Actions](./actions.md)). A pinned ref makes updates an explicit, reviewable action — bump the ref after reviewing the diff — instead of silently pulling whatever the catalog publishes next.
|
|
@@ -14,13 +14,14 @@ Resolve, compose, and launch an agent. `run` is the default command, so plain `o
|
|
|
14
14
|
| Argument / Option | Description |
|
|
15
15
|
| --------------------- | -------------------------------------------------------------------------------- |
|
|
16
16
|
| `[agent]` | Agent slug to run. Defaults to the settings `default_agent`. |
|
|
17
|
-
| `--harness <harness>` | Harness to launch in: `pi` or `
|
|
17
|
+
| `--harness <harness>` | Harness to launch in: `pi`, `claude`, or `codex`. Defaults to `default_harness`. |
|
|
18
18
|
| `--strict` | Fail instead of warning when the adapter cannot project part of the composition. |
|
|
19
19
|
|
|
20
20
|
Any other arguments and unrecognized options are passed through to the launched harness:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
outfitter run engineer --harness claude
|
|
24
|
+
outfitter run engineer --harness codex -- exec "review this repo"
|
|
24
25
|
outfitter run persona-reviewer -- --print "summarize this repo"
|
|
25
26
|
```
|
|
26
27
|
|
|
@@ -12,7 +12,7 @@ flowchart LR
|
|
|
12
12
|
adapter --> harness[Harness]
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Settings tell Outfitter where `.agents` resources come from; sources supply protocol resource trees; the resolver merges the layered trees into one effective resource set; the selected agent composes its loadout — skills, subagents, model, and so on — from that set by slug; and an adapter projects the composed agent into harness-specific files, flags, and environment variables before launching the harness (pi or
|
|
15
|
+
Settings tell Outfitter where `.agents` resources come from; sources supply protocol resource trees; the resolver merges the layered trees into one effective resource set; the selected agent composes its loadout — skills, subagents, model, and so on — from that set by slug; and an adapter projects the composed agent into harness-specific files, flags, and environment variables before launching the harness (pi, Claude Code, or Codex CLI).
|
|
16
16
|
|
|
17
17
|
## The `.agents` protocol
|
|
18
18
|
|
|
@@ -80,7 +80,7 @@ Listing, validation, running, and dumping all share one resolver. What `outfitte
|
|
|
80
80
|
|
|
81
81
|
## Adapters
|
|
82
82
|
|
|
83
|
-
An adapter projects composed resources into one agent CLI's native configuration — files, command-line flags, and environment variables. Pi is the primary and most complete adapter;
|
|
83
|
+
An adapter projects composed resources into one agent CLI's native configuration — files, command-line flags, and environment variables. Pi is the primary and most complete adapter; Claude Code and Codex CLI adapters are supported with gaps. When an adapter cannot honor part of a composition it warns to stderr, or fails when `--strict` is set. See the [adapter support matrix](./support-matrix.md).
|
|
84
84
|
|
|
85
85
|
## State persistence
|
|
86
86
|
|
|
@@ -6,10 +6,15 @@ The published image is a generic Outfitter runtime:
|
|
|
6
6
|
ghcr.io/ai-outfitter/outfitter:<version>
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
It uses `outfitter` as its entrypoint and
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
It is Debian-based (`node:24-slim`), uses `outfitter` as its entrypoint, and
|
|
10
|
+
includes Node.js, npm, Git, SSH, and CA certificates at their conventional
|
|
11
|
+
Debian paths. It does not include agent profiles, credentials, channels, MCP
|
|
12
|
+
servers, or other use-case behavior. The default runtime user and group are
|
|
13
|
+
both `1000` (named `outfitter`), with `/tmp` as the home directory and
|
|
14
|
+
`/workspace` as the working directory.
|
|
15
|
+
|
|
16
|
+
A Nix closure variant of the image is also published under the `-nix` suffix;
|
|
17
|
+
see [the `-nix` variant](#the--nix-variant) below.
|
|
13
18
|
|
|
14
19
|
## Run a resident agent
|
|
15
20
|
|
|
@@ -42,6 +47,7 @@ spec:
|
|
|
42
47
|
fsGroup: 1000
|
|
43
48
|
containers:
|
|
44
49
|
- name: agent
|
|
50
|
+
# The primary Debian-based image; append -nix for the Nix variant.
|
|
45
51
|
image: ghcr.io/ai-outfitter/outfitter:<version>
|
|
46
52
|
stdin: true
|
|
47
53
|
workingDir: /workspace
|
|
@@ -75,16 +81,58 @@ resources or impose image, profile, or extension policy.
|
|
|
75
81
|
|
|
76
82
|
## Build a derivative image
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
The image is a normal Debian base: extend it with an ordinary Dockerfile.
|
|
85
|
+
`apt-get` works, and so does `COPY`ing binaries. A dynamically linked binary
|
|
86
|
+
runs when it matches the image — same architecture, glibc-linked, and its
|
|
87
|
+
shared-library dependencies present. The standard ELF interpreter is where
|
|
88
|
+
tools expect it (unlike the `-nix` variant), but the slim base ships a small
|
|
89
|
+
library set: `apt-get install` a binary's runtime libraries when it needs more.
|
|
90
|
+
Switch to `root` for the layers that install, then drop back to `1000`:
|
|
91
|
+
|
|
92
|
+
```dockerfile
|
|
93
|
+
FROM ghcr.io/ai-outfitter/outfitter:<version>
|
|
94
|
+
|
|
95
|
+
USER root
|
|
96
|
+
RUN apt-get update \
|
|
97
|
+
&& apt-get install -y --no-install-recommends jq ripgrep \
|
|
98
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
99
|
+
COPY --chmod=0755 my-tool /usr/local/bin/my-tool
|
|
100
|
+
USER 1000
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Build and exercise the exact image:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
docker build -t example-agent .
|
|
107
|
+
docker run --rm example-agent --version
|
|
108
|
+
docker run --rm --entrypoint /bin/sh example-agent \
|
|
109
|
+
-c 'jq --version && rg --version && my-tool --version'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The entrypoint stays `outfitter`; override `ENTRYPOINT` only when the derived
|
|
113
|
+
image wraps the launch itself.
|
|
114
|
+
|
|
115
|
+
## The `-nix` variant
|
|
116
|
+
|
|
117
|
+
The Nix closure image that was previously the primary tag remains published
|
|
118
|
+
for `lib.mkContainer` consumers:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
ghcr.io/ai-outfitter/outfitter:<version>-nix
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
It is built by the flake, includes the Nix CLI, Bash, core utilities, Git,
|
|
125
|
+
SSH, and CA certificates, and its entrypoint is an absolute `/nix/store` path.
|
|
126
|
+
It is not conventionally extensible — there is no apt, and foreign dynamic
|
|
127
|
+
binaries do not run — so extend it through Nix instead: the flake exports
|
|
128
|
+
`lib.mkContainer` for reproducible derivative images:
|
|
81
129
|
|
|
82
130
|
```nix
|
|
83
131
|
{
|
|
84
132
|
inputs = {
|
|
85
133
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
86
134
|
outfitter = {
|
|
87
|
-
url = "github:ai-outfitter/outfitter/v1.
|
|
135
|
+
url = "github:ai-outfitter/outfitter/v1.4.0";
|
|
88
136
|
inputs.nixpkgs.follows = "nixpkgs";
|
|
89
137
|
};
|
|
90
138
|
};
|
|
@@ -122,13 +170,14 @@ docker run --rm --entrypoint /bin/sh example-agent:latest \
|
|
|
122
170
|
Prefer adding known runtime packages through `extraPackages`. The resulting
|
|
123
171
|
image stays reproducible, and it avoids the trap below.
|
|
124
172
|
|
|
125
|
-
**Do not mount an empty volume over `/nix
|
|
126
|
-
entrypoint is an absolute store path and every binary
|
|
127
|
-
into `/nix/store`. Mounting a fresh volume there hides
|
|
128
|
-
container cannot start — it fails before it could initialize
|
|
129
|
-
mounted the volume to populate.
|
|
173
|
+
**Do not mount an empty volume over `/nix` of the `-nix` variant.** That image
|
|
174
|
+
_is_ its Nix store: the entrypoint is an absolute store path and every binary
|
|
175
|
+
in `/bin` is a symlink into `/nix/store`. Mounting a fresh volume there hides
|
|
176
|
+
all of it, so the container cannot start — it fails before it could initialize
|
|
177
|
+
the very store you mounted the volume to populate. (The primary Debian image
|
|
178
|
+
has no `/nix` and is not affected.)
|
|
130
179
|
|
|
131
|
-
Runtime installation therefore needs one of:
|
|
180
|
+
Runtime installation in the `-nix` variant therefore needs one of:
|
|
132
181
|
|
|
133
182
|
- a volume **pre-populated** with the image's closure, seeded from the image
|
|
134
183
|
before the agent starts (an init container copying `/nix` into the volume);
|
|
@@ -15,6 +15,39 @@ See the [Claude Code hooks documentation](https://code.claude.com/docs/en/hooks)
|
|
|
15
15
|
|
|
16
16
|
Pi supports a bootstrap hook via its extension mechanism: an extension passed with `--extension` runs at session start and can register tools, providers, and runtime behavior. Outfitter's own onboarding flow uses this channel. For recurring per-event behavior, Pi extensions are the native surface.
|
|
17
17
|
|
|
18
|
+
## System extension hooks
|
|
19
|
+
|
|
20
|
+
An organization can make a local Pi observer load by default on every `outfitter run` by installing a system extension hook. Outfitter reads hook documents in lexical filename order from:
|
|
21
|
+
|
|
22
|
+
1. `$OUTFITTER_SYSTEM_DIR/*.yml` when the variable is set (the test/development seam), or
|
|
23
|
+
2. `/etc/outfitter/system.d/*.yml` on Linux, or
|
|
24
|
+
3. `/Library/Application Support/Outfitter/system.d/*.yml` on macOS.
|
|
25
|
+
|
|
26
|
+
Each file contributes extensions and environment additively; files do not override each other. Reusing the same environment key for the same harness in two files is an error rather than an implicit precedence rule. An absent directory is a no-op. An unreadable or malformed document, including one naming an extension path that does not exist, aborts the run whether or not `--strict` is set.
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
name: pensieve
|
|
30
|
+
harnesses:
|
|
31
|
+
pi:
|
|
32
|
+
extensions:
|
|
33
|
+
- /nix/store/example-pensieve/lib/pensieve/collectors/pi
|
|
34
|
+
env:
|
|
35
|
+
PENSIEVE_SINK: https://pensieve.example.com
|
|
36
|
+
PENSIEVE_INSTALL_SCOPE: launcher
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Extension entries must be absolute paths that already exist. Outfitter resolves source-directory, hook-document, and extension symlinks to physical paths before loading them; a dangling link is fatal, and `OUTFITTER_SYSTEM_HOOK_SOURCE` records the physical source directory. Package and Git specifiers are rejected, so a launch never installs a system extension from the network. Only `name` and `harnesses` are accepted at the document root, and each harness entry can contain only `extensions` and `env`; a system hook cannot select an agent, harness, model, tool, skill, or prompt.
|
|
40
|
+
|
|
41
|
+
For Pi, Outfitter prepends the configured `--extension <path>` arguments after projection on every launch, including `--mode rpc`, print, and other non-interactive launches. Hook documents cannot name the Outfitter-controlled `PI_CODING_AGENT_DIR` or `PI_CODING_AGENT_SESSION_DIR` variables. They also reject `NODE_OPTIONS`, `NODE_REPL_EXTERNAL_MODULE`, `OPENSSL_CONF`, `LD_PRELOAD`, `LD_AUDIT`, `LD_LIBRARY_PATH`, and every `DYLD_*` variable because those can change process loading before Pi starts. Other hook environment is below the launch plan's own environment but above the parent process environment at spawn. `harnesses.claude` and `harnesses.codex` documents validate but are ignored with a warning; Outfitter has no equivalent extension argument for those adapters. Their native managed configuration is the stronger policy surface.
|
|
42
|
+
|
|
43
|
+
### Launcher scope, not managed scope
|
|
44
|
+
|
|
45
|
+
This mechanism changes who owns the file that names an extension. It does not change Pi's configuration resolution because Pi never reads the system hook document. The accurate guarantee is **collection is on by default and the organization owns the configuration**, not that the session cannot turn collection off.
|
|
46
|
+
|
|
47
|
+
The `--no-extensions` option does not disable explicitly passed `--extension` paths, so it does not bypass a system hook. A session can still execute Outfitter's bundled Pi binary directly, never going through the Outfitter launcher, or set `OUTFITTER_SYSTEM_DIR` to an empty directory. Every launch whose platform resolves a hook directory records that choice in `OUTFITTER_SYSTEM_HOOK_SOURCE`: the normal value is the resolved physical system path, while an override is stamped as `env-override:<resolved-physical-path>`. Downstream evidence can therefore distinguish the normal system source from the session-settable bypass.
|
|
48
|
+
|
|
49
|
+
The normal Linux and macOS directories are root-owned. Outfitter deliberately fails closed on their operator errors: a malformed file should fail on a canary boot, while failing open could silently produce fleet sessions without collection. Those sessions must be treated as unattested rather than clean.
|
|
50
|
+
|
|
18
51
|
## Roadmap
|
|
19
52
|
|
|
20
53
|
> **TODO (protocol gap):** hooks are the one behavioral surface the pinned protocol revision does not model, which means hook definitions cannot yet be expressed portably in a `.agents` tree and projected per harness. The path `agents/<agent-id>/hooks/<hook-id>/` is reserved for a future agent-local hook entity and deliberately has no resolution or projection behavior today. Outfitter may need to ship its own hooks extension that adapters translate to Claude `settings.json` hooks and Pi extensions respectively, or drive the concept into a future protocol revision. Until one of those lands, treat hooks as harness-native configuration and keep them thin: call scripts that live in the tree rather than embedding logic in hook definitions.
|