@ai-outfitter/outfitter 1.5.0 → 1.7.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/DumpCommand.d.ts +1 -0
- package/dist/cli/commands/DumpCommand.js +11 -1
- package/dist/cli/commands/DumpCommand.js.map +1 -1
- package/dist/cli/commands/ListCommand.d.ts +2 -0
- package/dist/cli/commands/ListCommand.js +15 -4
- package/dist/cli/commands/ListCommand.js.map +1 -1
- package/dist/cli/commands/RunAgentCommand.js +85 -17
- package/dist/cli/commands/RunAgentCommand.js.map +1 -1
- package/dist/cli/commands/SyncCommand.d.ts +8 -3
- package/dist/cli/commands/SyncCommand.js +126 -21
- package/dist/cli/commands/SyncCommand.js.map +1 -1
- package/dist/cli/commands/ValidateCommand.js +1 -1
- package/dist/cli/commands/ValidateCommand.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 +7 -3
- package/dist/projection/Materialize.js.map +1 -1
- package/dist/projection/ProjectHarness.js +66 -29
- package/dist/projection/ProjectHarness.js.map +1 -1
- package/dist/projection/Projection.d.ts +2 -0
- package/dist/resolver/AmbiguityWarnings.d.ts +7 -0
- package/dist/resolver/AmbiguityWarnings.js +86 -0
- package/dist/resolver/AmbiguityWarnings.js.map +1 -0
- package/dist/resolver/Layer.d.ts +8 -2
- package/dist/resolver/Layer.js +48 -8
- package/dist/resolver/Layer.js.map +1 -1
- package/dist/resolver/ResolverContext.d.ts +3 -1
- package/dist/resolver/ResolverContext.js +11 -2
- 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/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 +45 -0
- package/dist/sources/TransitiveSources.js +179 -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 +14 -1
- package/docs/documentation/catalogs.md +61 -2
- package/docs/documentation/cli.md +2 -1
- package/docs/documentation/concepts.md +2 -2
- 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 +34 -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/settings.schema.json +1 -1
- package/src/schemas/system-extension-hook.schema.json +38 -0
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
// Encodes remote `.agents` source cache paths and normalizes remote source references.
|
|
2
|
-
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
2
|
+
import { isAbsolute, join, posix, relative, resolve } from 'node:path';
|
|
3
3
|
/** Distinguishes remote (`uri`/`github`) sources from local `path` sources. */
|
|
4
4
|
export const isRemoteSource = (source) => source.uri !== undefined || source.github !== undefined;
|
|
5
|
+
const fullCommitPattern = /^[a-f0-9]{40}$/u;
|
|
6
|
+
const versionTagPattern = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
7
|
+
/** True for a ref that names one immutable revision: a full commit SHA or a version tag. */
|
|
8
|
+
export const isImmutableRef = (ref) => fullCommitPattern.test(ref) || versionTagPattern.test(ref);
|
|
9
|
+
/** True for an immutable ref that must be resolved to a commit via `rev-parse` (a version tag). */
|
|
10
|
+
export const isVersionTagRef = (ref) => versionTagPattern.test(ref);
|
|
5
11
|
export const normalizeGitUri = (uri) => (uri.startsWith('git+') ? uri.slice('git+'.length) : uri);
|
|
6
12
|
export const normalizeRemoteSourceUri = (source) => {
|
|
7
13
|
if (source.uri !== undefined) {
|
|
@@ -32,9 +38,33 @@ export const redactSourceUriCredentials = (uri) => {
|
|
|
32
38
|
export const redactEmbeddedSourceCredentials = (value) => value.replace(/((?:git\+)?[A-Za-z][A-Za-z0-9+.-]*:\/\/)[^/@\s]+@/gu, '$1REDACTED@');
|
|
33
39
|
export const encodeSourceUri = (uri) => Buffer.from(redactSourceUriCredentials(uri), 'utf8').toString('base64url');
|
|
34
40
|
export const encodeRemoteSource = (source) => encodeSourceUri(`${normalizeRemoteSourceUri(source)}#${source.ref ?? ''}`);
|
|
41
|
+
// The payload subpath a source selects, canonicalized so identical selections share one identity:
|
|
42
|
+
// an omitted path, `.`, `./`, and `foo/..` all normalize to the repository root ('').
|
|
43
|
+
const canonicalSourcePath = (path) => {
|
|
44
|
+
if (path === undefined)
|
|
45
|
+
return '';
|
|
46
|
+
const normalized = posix.normalize(path).replace(/\/+$/u, '');
|
|
47
|
+
return normalized === '.' ? '' : normalized;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* A source's graph identity for dedup, visited sets, and layer ordering. It differs from
|
|
51
|
+
* {@link encodeRemoteSource} (the *checkout cache* key) in two deliberate ways:
|
|
52
|
+
*
|
|
53
|
+
* - It includes the canonicalized `path`, because two sources into different subpaths of one repo+ref
|
|
54
|
+
* are distinct layers — deduping them on the path-less cache key would silently drop one.
|
|
55
|
+
* - It keys on the *raw* normalized URI, credentials included, not the redacted cache key. Two entries
|
|
56
|
+
* for the same repo with different credentials are distinct configured sources; collapsing them
|
|
57
|
+
* (the cache key redacts credentials, so both share it) could drop a valid source for a broken one.
|
|
58
|
+
* This value is used only in ephemeral in-memory dedup/visited sets — never persisted or logged —
|
|
59
|
+
* so carrying credentials here is safe (the cache path and every user-facing display use the
|
|
60
|
+
* redacted forms).
|
|
61
|
+
*/
|
|
62
|
+
export const encodeRemoteSourceSelection = (source) => `${normalizeRemoteSourceUri(source)}#${source.ref ?? ''} ${canonicalSourcePath(source.path)}`;
|
|
35
63
|
/** Resolves the one cache root used by sync, settings, layer discovery, and setup bootstrap. */
|
|
36
64
|
export const resolveRemoteRepositoryCacheRoot = (homeDirectory, cacheDirectory) => cacheDirectory ?? join(homeDirectory, '.agents', 'cache');
|
|
37
65
|
export const createRemoteRepositoryCachePath = (homeDirectory, source, cacheDirectory) => join(resolveRemoteRepositoryCacheRoot(homeDirectory, cacheDirectory), 'repos', encodeRemoteSource(source));
|
|
66
|
+
/** The `.agents` payload root a source selects inside its materialized repository checkout. */
|
|
67
|
+
export const resolveSourcePayloadRoot = (repositoryPath, source) => source.path === undefined ? repositoryPath : resolveRemoteRepositorySubpath(repositoryPath, source.path);
|
|
38
68
|
export const resolveRemoteRepositorySubpath = (repositoryPath, subpath = '') => {
|
|
39
69
|
if (isAbsolute(subpath)) {
|
|
40
70
|
throw new Error(`Remote repository path '${subpath}' must be relative.`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SourceCache.js","sourceRoot":"","sources":["../../src/sources/SourceCache.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"SourceCache.js","sourceRoot":"","sources":["../../src/sources/SourceCache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { SourceReference } from '../settings/Settings.js';
|
|
2
|
+
import type { RemoteSourceReference } from './SourceCache.js';
|
|
3
|
+
/** A `github:` catalog dependency, pinned to an immutable ref, attributed to its declaring catalog. */
|
|
4
|
+
export interface DeclaredRemoteSource {
|
|
5
|
+
readonly source: {
|
|
6
|
+
readonly github: string;
|
|
7
|
+
readonly ref: string;
|
|
8
|
+
};
|
|
9
|
+
/** Display name of the declaring catalog, for warnings and provenance. */
|
|
10
|
+
readonly declaredBy: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DeclaredSourcesResult {
|
|
13
|
+
readonly sources: readonly DeclaredRemoteSource[];
|
|
14
|
+
readonly warnings: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
/** The settings file a catalog payload root may carry: `settings.yml`, else `.agents/settings.yml`. */
|
|
17
|
+
export declare const resolveCatalogSettingsPath: (payloadRoot: string) => string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Reads the `github:` catalog dependencies a cached catalog declares (content, never policy —
|
|
20
|
+
* OFTR-004.6.2). Transitive sources are deliberately the narrowest safe subset: a `github:`
|
|
21
|
+
* shorthand pinned to an immutable ref. A `uri:` source, a local `path:` source, a `path:` subpath,
|
|
22
|
+
* or a mutable ref is skipped with a warning naming the declaring catalog (OFTR-004.6.4,
|
|
23
|
+
* OFTR-004.6.5). Restricting to `github:` means every transitive fetch during `outfitter sync`
|
|
24
|
+
* passes through the private-catalog gate and can never select an arbitrary git transport (a `uri:`
|
|
25
|
+
* could name `ext::<cmd>` or a filesystem path); forbidding `path:` means a declaration can never
|
|
26
|
+
* escape its fetched checkout. An unreadable or invalid settings file skips the catalog's
|
|
27
|
+
* declarations without failing resolution (OFTR-004.6.8). (First-party default-catalog bootstrap
|
|
28
|
+
* fetches its declared closure without that interactive gate — see OFTR-004.6.10.) Broader
|
|
29
|
+
* transports remain future work under the trust model in #212.
|
|
30
|
+
*/
|
|
31
|
+
export declare const readDeclaredRemoteSources: (payloadRoot: string, checkoutRoot: string, declaredBy: string) => DeclaredSourcesResult;
|
|
32
|
+
export interface TransitiveExpansionInput {
|
|
33
|
+
/** The directly configured sources, which seed the visited set and the first frontier. */
|
|
34
|
+
readonly directSources: readonly SourceReference[];
|
|
35
|
+
/** Maps a remote source to its cached checkout (clone) root, or undefined when it is not cached. */
|
|
36
|
+
readonly resolveCachedCheckoutRoot: (source: RemoteSourceReference) => string | undefined;
|
|
37
|
+
}
|
|
38
|
+
export interface TransitiveExpansionResult {
|
|
39
|
+
/** Newly discovered remote sources, breadth-first by depth then declaration order (OFTR-004.6.3). */
|
|
40
|
+
readonly sources: readonly DeclaredRemoteSource[];
|
|
41
|
+
/** Every accepted declaration, including duplicates suppressed from the resolved source graph. */
|
|
42
|
+
readonly declarations: readonly DeclaredRemoteSource[];
|
|
43
|
+
readonly warnings: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
export declare const expandTransitiveSources: (input: TransitiveExpansionInput) => TransitiveExpansionResult;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Expands the remote-source closure that cached catalogs declare in their own settings files.
|
|
2
|
+
import { existsSync, lstatSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join, relative, sep } from 'node:path';
|
|
4
|
+
import { createSettingsLoadPlan, loadSettingsFiles } from '../settings/SettingsLoader.js';
|
|
5
|
+
import { encodeRemoteSourceSelection, formatRemoteSourceDisplay, isImmutableRef, isRemoteSource, resolveSourcePayloadRoot, } from './SourceCache.js';
|
|
6
|
+
// True when a filesystem entry exists at the path, *including a dangling symlink*. `existsSync`
|
|
7
|
+
// follows links and reports a dangling one as absent, which would silently drop (rather than warn
|
|
8
|
+
// about, per OFTR-004.6.8) a `settings.yml` committed as a broken symlink.
|
|
9
|
+
const entryExists = (path) => {
|
|
10
|
+
try {
|
|
11
|
+
lstatSync(path);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/** The settings file a catalog payload root may carry: `settings.yml`, else `.agents/settings.yml`. */
|
|
19
|
+
export const resolveCatalogSettingsPath = (payloadRoot) => {
|
|
20
|
+
const rootSettingsPath = join(payloadRoot, 'settings.yml');
|
|
21
|
+
if (entryExists(rootSettingsPath))
|
|
22
|
+
return rootSettingsPath;
|
|
23
|
+
const nestedSettingsPath = join(payloadRoot, '.agents', 'settings.yml');
|
|
24
|
+
return entryExists(nestedSettingsPath) ? nestedSettingsPath : undefined;
|
|
25
|
+
};
|
|
26
|
+
/** Applies the safe-subset rules to one declared source, yielding a dependency or a skip reason. */
|
|
27
|
+
const classifyDeclaredSource = (declared, declaredBy) => {
|
|
28
|
+
if (!isRemoteSource(declared)) {
|
|
29
|
+
return {
|
|
30
|
+
skip: `Catalog '${declaredBy}' declares local path source '${declared.path}'; only 'github:' shorthands are resolved transitively, so it was skipped.`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (declared.github === undefined) {
|
|
34
|
+
return {
|
|
35
|
+
skip: `Catalog '${declaredBy}' declares source '${formatRemoteSourceDisplay(declared)}' as a 'uri:'; only 'github:' shorthands are resolved transitively, so it was skipped.`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (declared.path !== undefined) {
|
|
39
|
+
return {
|
|
40
|
+
skip: `Catalog '${declaredBy}' declares source '${formatRemoteSourceDisplay(declared)}' with a 'path:' subpath; transitive 'github:' sources must resolve their whole repository, so it was skipped.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (declared.ref === undefined || !isImmutableRef(declared.ref)) {
|
|
44
|
+
return {
|
|
45
|
+
skip: `Catalog '${declaredBy}' declares source '${formatRemoteSourceDisplay(declared)}' without an immutable ref (a full commit SHA or version tag); it was skipped.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { source: { github: declared.github, ref: declared.ref } };
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Loads a cached catalog's own settings file, guarding against a hostile `settings.yml` committed as
|
|
52
|
+
* a symlink to a directory or a file outside the checkout (a `readFileSync` on a directory throws
|
|
53
|
+
* `EISDIR` and would crash resolution). Returns the parsed sources list, or a skip warning.
|
|
54
|
+
*/
|
|
55
|
+
const loadCatalogSourceList = (payloadRoot, checkoutRoot, declaredBy) => {
|
|
56
|
+
const settingsPath = resolveCatalogSettingsPath(payloadRoot);
|
|
57
|
+
if (settingsPath === undefined)
|
|
58
|
+
return { sources: [] };
|
|
59
|
+
try {
|
|
60
|
+
if (!statSync(settingsPath).isFile())
|
|
61
|
+
throw new Error('settings.yml is not a regular file');
|
|
62
|
+
// Anchor containment to the *checkout* root (the clone Outfitter created), not the payload root:
|
|
63
|
+
// a `path:` selecting a symlinked subdirectory would make the payload root itself resolve
|
|
64
|
+
// outside the clone, so anchoring there would accept an external settings.yml as "contained".
|
|
65
|
+
const relativeToCheckout = relative(realpathSync(checkoutRoot), realpathSync(settingsPath));
|
|
66
|
+
// Precise containment: a leading `..` segment (not a filename that merely starts with `..`, such
|
|
67
|
+
// as a directory named `..catalog`) or an absolute result means the settings escaped the checkout.
|
|
68
|
+
if (relativeToCheckout === '..' || relativeToCheckout.startsWith(`..${sep}`) || isAbsolute(relativeToCheckout)) {
|
|
69
|
+
throw new Error('settings.yml resolves outside the checkout');
|
|
70
|
+
}
|
|
71
|
+
// Known-accepted TOCTOU: the checkout could be swapped between this stat/realpath check and the
|
|
72
|
+
// read below. The checkout is a pinned immutable git ref that only Outfitter writes, and racing
|
|
73
|
+
// local-filesystem writes already imply broader access, so closing this window is not warranted.
|
|
74
|
+
const loaded = loadSettingsFiles(createSettingsLoadPlan([{ scope: 'remote', path: settingsPath }]));
|
|
75
|
+
if (loaded.issues.length > 0) {
|
|
76
|
+
return {
|
|
77
|
+
warning: `Catalog '${declaredBy}' declares invalid settings at '${settingsPath}'; its sources were skipped.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { sources: loaded.files[0].settings.sources ?? [] };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return {
|
|
84
|
+
warning: `Catalog '${declaredBy}' has an unreadable settings.yml (not a regular file inside the checkout); its sources were skipped.`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Reads the `github:` catalog dependencies a cached catalog declares (content, never policy —
|
|
90
|
+
* OFTR-004.6.2). Transitive sources are deliberately the narrowest safe subset: a `github:`
|
|
91
|
+
* shorthand pinned to an immutable ref. A `uri:` source, a local `path:` source, a `path:` subpath,
|
|
92
|
+
* or a mutable ref is skipped with a warning naming the declaring catalog (OFTR-004.6.4,
|
|
93
|
+
* OFTR-004.6.5). Restricting to `github:` means every transitive fetch during `outfitter sync`
|
|
94
|
+
* passes through the private-catalog gate and can never select an arbitrary git transport (a `uri:`
|
|
95
|
+
* could name `ext::<cmd>` or a filesystem path); forbidding `path:` means a declaration can never
|
|
96
|
+
* escape its fetched checkout. An unreadable or invalid settings file skips the catalog's
|
|
97
|
+
* declarations without failing resolution (OFTR-004.6.8). (First-party default-catalog bootstrap
|
|
98
|
+
* fetches its declared closure without that interactive gate — see OFTR-004.6.10.) Broader
|
|
99
|
+
* transports remain future work under the trust model in #212.
|
|
100
|
+
*/
|
|
101
|
+
export const readDeclaredRemoteSources = (payloadRoot, checkoutRoot, declaredBy) => {
|
|
102
|
+
const loaded = loadCatalogSourceList(payloadRoot, checkoutRoot, declaredBy);
|
|
103
|
+
if ('warning' in loaded)
|
|
104
|
+
return { sources: [], warnings: [loaded.warning] };
|
|
105
|
+
const sources = [];
|
|
106
|
+
const warnings = [];
|
|
107
|
+
for (const declared of loaded.sources) {
|
|
108
|
+
const classified = classifyDeclaredSource(declared, declaredBy);
|
|
109
|
+
if ('skip' in classified)
|
|
110
|
+
warnings.push(classified.skip);
|
|
111
|
+
else
|
|
112
|
+
sources.push({ source: classified.source, declaredBy });
|
|
113
|
+
}
|
|
114
|
+
return { sources, warnings };
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Walks declared sources breadth-first from the directly configured sources over the local cache.
|
|
118
|
+
* A source already visited — directly configured or declared by an earlier catalog — is never
|
|
119
|
+
* expanded again, so dependency cycles terminate (OFTR-004.6.6). An uncached source stays in the
|
|
120
|
+
* result (the caller reports it) but its own declarations are unknowable until it is synced.
|
|
121
|
+
*/
|
|
122
|
+
// Reads one parent's cached checkout and returns the declarations it contributes. Returns `undefined`
|
|
123
|
+
// (skip this parent) when it is uncached or its `path:` is absolute/escaping — a throw here would
|
|
124
|
+
// otherwise crash resolution; only a directly configured source can carry such a path.
|
|
125
|
+
const declarationsFromParent = (parent, resolveCachedCheckoutRoot) => {
|
|
126
|
+
const checkoutRoot = resolveCachedCheckoutRoot(parent);
|
|
127
|
+
if (checkoutRoot === undefined)
|
|
128
|
+
return undefined;
|
|
129
|
+
let payloadRoot;
|
|
130
|
+
try {
|
|
131
|
+
payloadRoot = resolveSourcePayloadRoot(checkoutRoot, parent);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
if (!existsSync(payloadRoot))
|
|
137
|
+
return undefined;
|
|
138
|
+
return readDeclaredRemoteSources(payloadRoot, checkoutRoot, formatRemoteSourceDisplay(parent));
|
|
139
|
+
};
|
|
140
|
+
export const expandTransitiveSources = (input) => {
|
|
141
|
+
const sources = [];
|
|
142
|
+
const declarations = [];
|
|
143
|
+
const warnings = [];
|
|
144
|
+
const visited = new Set();
|
|
145
|
+
// Seed the visited set with each direct source's path-aware identity so a directly configured
|
|
146
|
+
// subpath source does not suppress a transitive whole-repository source of the same repo+ref, and
|
|
147
|
+
// so an exact duplicate direct source is expanded only once (OFTR-004.6.6).
|
|
148
|
+
let frontier = [];
|
|
149
|
+
for (const direct of input.directSources) {
|
|
150
|
+
if (!isRemoteSource(direct))
|
|
151
|
+
continue;
|
|
152
|
+
const key = encodeRemoteSourceSelection(direct);
|
|
153
|
+
if (visited.has(key))
|
|
154
|
+
continue;
|
|
155
|
+
visited.add(key);
|
|
156
|
+
frontier.push(direct);
|
|
157
|
+
}
|
|
158
|
+
while (frontier.length > 0) {
|
|
159
|
+
const next = [];
|
|
160
|
+
for (const parent of frontier) {
|
|
161
|
+
const declared = declarationsFromParent(parent, input.resolveCachedCheckoutRoot);
|
|
162
|
+
if (declared === undefined)
|
|
163
|
+
continue;
|
|
164
|
+
warnings.push(...declared.warnings);
|
|
165
|
+
for (const entry of declared.sources) {
|
|
166
|
+
declarations.push(entry);
|
|
167
|
+
const key = encodeRemoteSourceSelection(entry.source);
|
|
168
|
+
if (visited.has(key))
|
|
169
|
+
continue;
|
|
170
|
+
visited.add(key);
|
|
171
|
+
sources.push(entry);
|
|
172
|
+
next.push(entry.source);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
frontier = next;
|
|
176
|
+
}
|
|
177
|
+
return { sources, declarations, warnings };
|
|
178
|
+
};
|
|
179
|
+
//# sourceMappingURL=TransitiveSources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TransitiveSources.js","sourceRoot":"","sources":["../../src/sources/TransitiveSources.ts"],"names":[],"mappings":"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"}
|
|
@@ -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
|