@aiwg/cli 2026.10.6 → 2026.10.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/aiwg.mjs +4 -2
- package/dist/src/a2a/agent-card.js +11 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
- package/dist/src/artifacts/fortemi-shard-export.js +284 -0
- package/dist/src/artifacts/index-preparation.js +2 -0
- package/dist/src/auth/client.js +5 -1
- package/dist/src/auth/credential-store.js +5 -1
- package/dist/src/auth/sensitive-input.js +151 -0
- package/dist/src/channel/manager.mjs +3 -0
- package/dist/src/cli/handlers/agentcard.js +47 -25
- package/dist/src/cli/handlers/auth.js +22 -2
- package/dist/src/cli/handlers/decision.js +3 -2
- package/dist/src/cli/handlers/effect.js +4 -84
- package/dist/src/cli/handlers/guided-setup.js +1 -1
- package/dist/src/cli/handlers/init.js +1 -1
- package/dist/src/cli/handlers/install.js +6 -0
- package/dist/src/cli/handlers/marketplace.js +15 -4
- package/dist/src/cli/handlers/setup.js +24 -20
- package/dist/src/cli/handlers/use.js +57 -15
- package/dist/src/cli/prompt-utils.js +73 -0
- package/dist/src/config/gitignore.js +4 -0
- package/dist/src/config/workspace.js +2 -1
- package/dist/src/decision/adapters/jev.js +20 -31
- package/dist/src/decision/ask.js +6 -1
- package/dist/src/decision/benchmark.js +19 -6
- package/dist/src/decision/qualification/gate-evidence.js +6 -4
- package/dist/src/decision/qualification/load-staged.js +47 -10
- package/dist/src/decision/qualification/runner.js +7 -3
- package/dist/src/discovery/interview.js +10 -5
- package/dist/src/discovery/plan-apply.js +26 -30
- package/dist/src/discovery/plan-validation.js +2 -2
- package/dist/src/effects/host.js +91 -0
- package/dist/src/extensions/commands/definitions.js +2 -2
- package/dist/src/extensions/project-local-gitignore.js +2 -0
- package/dist/src/issues/cli.js +16 -5
- package/dist/src/issues/live.js +44 -12
- package/dist/src/marketplace/exchange.js +18 -6
- package/dist/src/marketplace/provenance.js +3 -2
- package/dist/src/mcp/adapters/codex-runtime.js +46 -76
- package/dist/src/mcp/cli.mjs +17 -90
- package/dist/src/mcp/grok-build-config.mjs +2 -1
- package/dist/src/mcp/muse-native-config.mjs +2 -1
- package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
- package/dist/src/memory/project-registry.js +2 -1
- package/dist/src/mission-protocol/worker-status.js +91 -1
- package/dist/src/models/worker-installation.d.mts +1 -1
- package/dist/src/models/worker-installation.mjs +65 -4
- package/dist/src/packages/adapters/git.js +99 -23
- package/dist/src/packages/adapters/gitea.js +8 -27
- package/dist/src/packages/adapters/github.js +7 -25
- package/dist/src/packages/registry.js +4 -4
- package/dist/src/process/cli-command.d.mts +5 -0
- package/dist/src/process/cli-command.mjs +42 -0
- package/dist/src/serve/worker-invocation.js +10 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
- package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
- package/dist/src/smiths/context-pipeline/finalization.js +3 -2
- package/dist/src/smiths/context-pipeline/generator.js +7 -19
- package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
- package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
- package/dist/src/tracker/project-inputs.js +2 -0
- package/dist/src/update/notifier.mjs +2 -4
- package/package.json +1 -1
- package/tools/agents/providers/base.mjs +5 -1
- package/tools/agents/providers/claude.mjs +5 -2
- package/tools/agents/providers/codex.mjs +4 -1
- package/tools/agents/providers/context-write.mjs +28 -0
- package/tools/agents/providers/copilot.mjs +5 -2
- package/tools/agents/providers/hermes.mjs +5 -2
- package/tools/agents/providers/omp.mjs +3 -1
- package/tools/agents/providers/rule-delivery.mjs +29 -12
- package/tools/agents/providers/warp.mjs +4 -1
- package/tools/agents/providers/windsurf.mjs +6 -3
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
- package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
- package/tools/ralph-external/process-monitor.mjs +42 -4
- package/tools/ralph-external/session-launcher.mjs +125 -3
|
@@ -824,6 +824,40 @@ export async function prepareWorkerInstallationStore({ workerWritableRoots, appl
|
|
|
824
824
|
return assertTrustedStore(installationStorePath(), workerWritableRoots, true, apply);
|
|
825
825
|
}
|
|
826
826
|
|
|
827
|
+
// Use the existing delivery ledger's host-selected keyring, never public keys
|
|
828
|
+
// supplied by a registration. PAE separates this authority from effect records.
|
|
829
|
+
const qualificationPayloadType = 'application/vnd.aiwg.worker-installation-qualification+json';
|
|
830
|
+
const requalifyMessage = 'Re-run aiwg doctor --probe-workers --qualify. Existing unsigned approvals require deliberate one-time requalification.';
|
|
831
|
+
const keyRecoveryMessage = 'Worker qualification signing trust is unavailable. Check effects.keyProvider in aiwg.config, unlock or restore the existing effect-ledger key, and run aiwg effect keys init if its keyring is not initialized; then re-run aiwg doctor --probe-workers --qualify.';
|
|
832
|
+
|
|
833
|
+
async function qualificationTrust(cwd, scope) {
|
|
834
|
+
const { effectSession, openLedger } = await import('../effects/host.js');
|
|
835
|
+
const { openEffectLedger, readLedgerKeyring } = await import('../effects/ledger.js');
|
|
836
|
+
const ledger = scope ? openEffectLedger({ projectDir: cwd, scope, writer: 'cli' }) : openLedger(await effectSession(cwd));
|
|
837
|
+
const keyring = await readLedgerKeyring(ledger);
|
|
838
|
+
if (!keyring) throw new Error(keyRecoveryMessage);
|
|
839
|
+
return { ledger, keyring };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async function qualificationPayload(record, signedAt, projectDir, scope) {
|
|
843
|
+
const { canonicalJson } = await import('../security/artifact-trust.js');
|
|
844
|
+
const { signature: _signature, ...body } = record;
|
|
845
|
+
return Buffer.from(canonicalJson({ record: JSON.parse(JSON.stringify(body)), signedAt, projectDir, scope }), 'utf8');
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async function qualificationSignatureFailure(record, trust) {
|
|
849
|
+
if (!record.signature) return 'signature-missing';
|
|
850
|
+
const { keyid, sig, signedAt, projectDir, scope } = record.signature;
|
|
851
|
+
if (typeof keyid !== 'string' || typeof sig !== 'string' || typeof signedAt !== 'string'
|
|
852
|
+
|| typeof projectDir !== 'string' || !path.isAbsolute(projectDir) || !scope) return 'signature-invalid';
|
|
853
|
+
let keyring;
|
|
854
|
+
try { ({ keyring } = await trust(projectDir, scope)); } catch { return 'signature-trust-unavailable'; }
|
|
855
|
+
const { checkSignature } = await import('../effects/keyring.js');
|
|
856
|
+
const result = checkSignature(keyring, keyid, sig, qualificationPayloadType,
|
|
857
|
+
await qualificationPayload(record, signedAt, projectDir, scope), signedAt);
|
|
858
|
+
return result === 'ok' ? null : result === 'signature-invalid' ? result : `signature-${result}`;
|
|
859
|
+
}
|
|
860
|
+
|
|
827
861
|
async function readInstallationStore(filename, expectedDigest) {
|
|
828
862
|
try {
|
|
829
863
|
const bytes = await readInstalledFile(filename);
|
|
@@ -864,6 +898,7 @@ export async function inspectRecordedWorkerQualifications({ cwd, policy, paralle
|
|
|
864
898
|
|| (configured.executable && await realpath(configured.executable) !== await realpath(installation.executable))) continue;
|
|
865
899
|
const proof = adapter.invocationProfiles?.[binding.profile]?.installationQualification;
|
|
866
900
|
if (!proof || ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef'].some(key => proof[key] !== installation.evidence?.[key])) continue;
|
|
901
|
+
if (await qualificationSignatureFailure(record, () => qualificationTrust(cwd))) continue;
|
|
867
902
|
await assertWorkerInstallationEvidence({ ...installation, adapter, now: Date.now(), policy }, proof.scopeDigest);
|
|
868
903
|
results.push({ cli, provider: adapter.provider });
|
|
869
904
|
} catch { /* Malformed, expired or unbound evidence is not qualification. */ }
|
|
@@ -879,6 +914,15 @@ export async function recordWorkerInstallation(instance, options) {
|
|
|
879
914
|
const installation = structuredClone(options.installation);
|
|
880
915
|
const result = await qualifyWorkerInstallation(instance, { ...installation, now: Date.now() });
|
|
881
916
|
if (result.status !== 'ENABLED') return result;
|
|
917
|
+
let signingKey, keyring, signingProjectDir;
|
|
918
|
+
try {
|
|
919
|
+
signingProjectDir = await realpath(options.projectDir ?? installation.cwd);
|
|
920
|
+
const trust = await qualificationTrust(signingProjectDir);
|
|
921
|
+
keyring = trust.keyring;
|
|
922
|
+
signingKey = await trust.ledger.signingKey();
|
|
923
|
+
const { activeKey } = await import('../effects/keyring.js');
|
|
924
|
+
if (activeKey(keyring).keyid !== signingKey.keyid) throw new Error('inactive key');
|
|
925
|
+
} catch { return { status: 'DISABLED', reason: keyRecoveryMessage }; }
|
|
882
926
|
const adapter = structuredClone(installation.adapter);
|
|
883
927
|
adapter.invocationProfiles[installation.profile].installationQualification = {
|
|
884
928
|
scopeDigest: result.scopeDigest, fixtureDigest: result.fixtureDigest,
|
|
@@ -913,6 +957,14 @@ export async function recordWorkerInstallation(instance, options) {
|
|
|
913
957
|
installation: { ...installation, environmentNames, versionProbe: { rawBanner: result.rawVersionBanner, version: result.cliVersion } },
|
|
914
958
|
...(options.mcpSchemas ? { mcpSchemas: structuredClone(options.mcpSchemas) } : {}),
|
|
915
959
|
...(options.mcpProfile ? { mcpProfile: structuredClone(options.mcpProfile) } : {}) };
|
|
960
|
+
const signedAt = new Date().toISOString();
|
|
961
|
+
try {
|
|
962
|
+
const payload = await qualificationPayload(record, signedAt, signingProjectDir, keyring.scope);
|
|
963
|
+
const sig = signingKey.signPae(qualificationPayloadType, payload);
|
|
964
|
+
const { checkSignature } = await import('../effects/keyring.js');
|
|
965
|
+
if (checkSignature(keyring, signingKey.keyid, sig, qualificationPayloadType, payload, signedAt) !== 'ok') throw new Error('invalid signature');
|
|
966
|
+
record.signature = { keyid: signingKey.keyid, sig, signedAt, projectDir: signingProjectDir, scope: keyring.scope };
|
|
967
|
+
} catch { return { status: 'DISABLED', reason: keyRecoveryMessage }; }
|
|
916
968
|
const retained = [];
|
|
917
969
|
for (const entry of stored.registrations) {
|
|
918
970
|
const workspace = typeof entry.installation?.cwd === 'string'
|
|
@@ -944,7 +996,8 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, p
|
|
|
944
996
|
// Underlying errors can contain probe output, environment values or paths.
|
|
945
997
|
const rejections = [];
|
|
946
998
|
const fail = () => new WorkerPolicyError('OG_ADAPTER_UNQUALIFIED',
|
|
947
|
-
`Trusted installation qualification is required (${[...new Set(rejections.map(row => row.reason))].join(', ')})
|
|
999
|
+
`Trusted installation qualification is required (${[...new Set(rejections.map(row => row.reason))].join(', ')})${rejections.some(row => row.reason.startsWith('signature-'))
|
|
1000
|
+
? `. ${rejections.some(row => row.reason === 'signature-trust-unavailable') ? keyRecoveryMessage : requalifyMessage}` : ''}`,
|
|
948
1001
|
{ rejections });
|
|
949
1002
|
let reason = 'store-binding';
|
|
950
1003
|
let filename;
|
|
@@ -968,6 +1021,7 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, p
|
|
|
968
1021
|
const installations = {};
|
|
969
1022
|
const drivers = {};
|
|
970
1023
|
const observations = {};
|
|
1024
|
+
const qualificationFailures = Object.create(null);
|
|
971
1025
|
const resolvedRegistry = structuredClone(registry);
|
|
972
1026
|
const requestedWorkspace = await realpath(cwd);
|
|
973
1027
|
const workspaces = await Promise.all(stored.registrations.map(async record => {
|
|
@@ -990,6 +1044,11 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, p
|
|
|
990
1044
|
if (!/^[a-z][a-z0-9-]{0,62}$/.test(cli)) throw fail();
|
|
991
1045
|
if (counts.get(JSON.stringify([cli, workspaces[registration]])) !== 1) throw fail();
|
|
992
1046
|
if (selected.get(cli) !== registration) continue;
|
|
1047
|
+
// A conductor-pinned store digest also binds the original config source.
|
|
1048
|
+
// Framing from a worker cwd must never read that cwd's configuration.
|
|
1049
|
+
reason = await qualificationSignatureFailure(record, (projectDir, scope) => storePath === undefined
|
|
1050
|
+
? qualificationTrust(cwd) : qualificationTrust(projectDir, scope));
|
|
1051
|
+
if (reason) throw fail();
|
|
993
1052
|
const installation = { ...record.installation, adapter, now: Date.now(), environment: {} };
|
|
994
1053
|
reason = 'environment-name';
|
|
995
1054
|
if (record.installation.environment && Object.keys(record.installation.environment).length) throw fail();
|
|
@@ -1106,13 +1165,15 @@ export async function loadRecordedWorkerInstallations({ registry, cwd, policy, p
|
|
|
1106
1165
|
Object.assign(resolvedRegistry, candidateRegistry);
|
|
1107
1166
|
} catch (error) {
|
|
1108
1167
|
// One rejected installation cannot grant authority or hide qualified peers.
|
|
1109
|
-
|
|
1110
|
-
? 'adapter-conflict' : reason
|
|
1168
|
+
const failureReason = reason === 'module-registration' && error?.code === 'WORKER_ADAPTER_CONFLICT'
|
|
1169
|
+
? 'adapter-conflict' : reason;
|
|
1170
|
+
rejections.push({ registration, reason: failureReason });
|
|
1171
|
+
if (typeof record?.cli === 'string' && /^[a-z][a-z0-9-]{0,62}$/.test(record.cli)) qualificationFailures[record.cli] = failureReason;
|
|
1111
1172
|
}
|
|
1112
1173
|
}
|
|
1113
1174
|
if (!Object.keys(installations).length) {
|
|
1114
1175
|
if (!rejections.length) rejections.push({ registration: null, reason: 'no-records' });
|
|
1115
1176
|
throw fail();
|
|
1116
1177
|
}
|
|
1117
|
-
return { registry: resolvedRegistry, installations, drivers, observations };
|
|
1178
|
+
return { registry: resolvedRegistry, installations, drivers, observations, qualificationFailures };
|
|
1118
1179
|
}
|
|
@@ -11,11 +11,40 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { execFile } from 'child_process';
|
|
13
13
|
import { promisify } from 'util';
|
|
14
|
-
import { mkdir, mkdtemp, readFile, rename, rm } from 'fs/promises';
|
|
14
|
+
import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'fs/promises';
|
|
15
15
|
import { join } from 'path';
|
|
16
|
-
import { homedir } from 'os';
|
|
16
|
+
import { homedir, tmpdir } from 'os';
|
|
17
17
|
import { existsSync } from 'fs';
|
|
18
|
+
import { resolveSensitiveInput } from '../../auth/sensitive-input.js';
|
|
19
|
+
import { helperEnvironment, HELPER_ENV_ALLOWLIST } from '../../process/cli-command.mjs';
|
|
18
20
|
const execFileAsync = promisify(execFile);
|
|
21
|
+
// Deliberately separate from serializable sources, locks and provenance.
|
|
22
|
+
const credentials = new WeakMap();
|
|
23
|
+
function hasTokenSource(input) {
|
|
24
|
+
return Boolean(input.interactive || input.stdin || input.file !== undefined || input.store !== undefined || input.env !== undefined);
|
|
25
|
+
}
|
|
26
|
+
/** Explicit sources precede the established env/file lookup; no token means SSH. */
|
|
27
|
+
export async function resolvePackageToken(input = {}, envName, paths = []) {
|
|
28
|
+
if (hasTokenSource(input))
|
|
29
|
+
return resolveSensitiveInput({ ...input, prefix: 'token' });
|
|
30
|
+
if (envName && process.env[envName])
|
|
31
|
+
return resolveSensitiveInput({ prefix: 'token', env: envName });
|
|
32
|
+
for (const file of paths) {
|
|
33
|
+
if (existsSync(file))
|
|
34
|
+
return resolveSensitiveInput({ prefix: 'token', file });
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
// GitHub rejects Bearer on git smart HTTP; Basic with x-access-token works on GitHub and Gitea.
|
|
39
|
+
function basicCredential(token) {
|
|
40
|
+
return Buffer.from(`x-access-token:${token}`).toString('base64');
|
|
41
|
+
}
|
|
42
|
+
function redact(value, token) {
|
|
43
|
+
if (!token)
|
|
44
|
+
return value;
|
|
45
|
+
return [token, encodeURIComponent(token), basicCredential(token)]
|
|
46
|
+
.reduce((result, secret) => result.split(secret).join('[REDACTED]'), value);
|
|
47
|
+
}
|
|
19
48
|
/**
|
|
20
49
|
* Default cache root
|
|
21
50
|
*/
|
|
@@ -34,16 +63,24 @@ export function buildCachePath(owner, name, version) {
|
|
|
34
63
|
/**
|
|
35
64
|
* Run a git command, returning stdout
|
|
36
65
|
*/
|
|
37
|
-
async function git(args, cwd) {
|
|
38
|
-
const env =
|
|
66
|
+
async function git(args, cwd, authentication) {
|
|
67
|
+
const env = helperEnvironment(process.env, [...HELPER_ENV_ALLOWLIST, 'SSH_AUTH_SOCK', 'SSH_AGENT_PID',
|
|
68
|
+
'GIT_SSH', 'GIT_SSH_COMMAND', 'GIT_SSH_VARIANT', 'XDG_CONFIG_HOME']);
|
|
39
69
|
// Suppress interactive prompts
|
|
40
70
|
env.GIT_TERMINAL_PROMPT = '0';
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
env
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
71
|
+
if (authentication) {
|
|
72
|
+
env.GIT_CONFIG_COUNT = '1';
|
|
73
|
+
env.GIT_CONFIG_KEY_0 = 'include.path';
|
|
74
|
+
env.GIT_CONFIG_VALUE_0 = authentication.config;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const { stdout } = await execFileAsync('git', args, { cwd, env, timeout: 120_000 });
|
|
78
|
+
return redact(stdout.trim(), authentication?.token);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
// Never rethrow execFile's object: it carries raw stdout/stderr properties.
|
|
82
|
+
throw new Error(redact(error instanceof Error ? error.message : 'Git command failed.', authentication?.token));
|
|
83
|
+
}
|
|
47
84
|
}
|
|
48
85
|
/**
|
|
49
86
|
* Detect the manifest type from a cloned package directory
|
|
@@ -69,9 +106,9 @@ async function detectManifestType(cachePath) {
|
|
|
69
106
|
/**
|
|
70
107
|
* Resolve the latest tag from a remote git repo
|
|
71
108
|
*/
|
|
72
|
-
async function resolveLatestTag(gitUrl) {
|
|
109
|
+
async function resolveLatestTag(gitUrl, authentication) {
|
|
73
110
|
try {
|
|
74
|
-
const output = await git(['ls-remote', '--tags', '--sort=-v:refname', gitUrl]);
|
|
111
|
+
const output = await git(['ls-remote', '--tags', '--sort=-v:refname', gitUrl], undefined, authentication);
|
|
75
112
|
const firstLine = output.split('\n')[0] ?? '';
|
|
76
113
|
const match = firstLine.match(/refs\/tags\/(.+)/);
|
|
77
114
|
if (match && match[1])
|
|
@@ -82,14 +119,14 @@ async function resolveLatestTag(gitUrl) {
|
|
|
82
119
|
}
|
|
83
120
|
return 'latest';
|
|
84
121
|
}
|
|
85
|
-
async function resolveRemoteCommit(gitUrl, requestedRef) {
|
|
122
|
+
async function resolveRemoteCommit(gitUrl, requestedRef, authentication) {
|
|
86
123
|
if (/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(requestedRef))
|
|
87
124
|
return requestedRef;
|
|
88
125
|
const patterns = requestedRef === 'HEAD'
|
|
89
126
|
? ['HEAD']
|
|
90
127
|
: [`refs/tags/${requestedRef}^{}`, `refs/tags/${requestedRef}`, `refs/heads/${requestedRef}`, requestedRef];
|
|
91
128
|
try {
|
|
92
|
-
const output = await git(['ls-remote', gitUrl, ...patterns]);
|
|
129
|
+
const output = await git(['ls-remote', gitUrl, ...patterns], undefined, authentication);
|
|
93
130
|
const rows = output.split('\n').filter(Boolean).map((line) => {
|
|
94
131
|
const [oid = '', ref = ''] = line.split(/\s+/, 2);
|
|
95
132
|
return { oid, ref };
|
|
@@ -136,30 +173,69 @@ export class GitAdapter {
|
|
|
136
173
|
ref.startsWith('ssh://') ||
|
|
137
174
|
ref.startsWith('git+https://'));
|
|
138
175
|
}
|
|
139
|
-
async resolve(ref) {
|
|
176
|
+
async resolve(ref, options = {}) {
|
|
140
177
|
if (!ref.rawUrl)
|
|
141
178
|
return null;
|
|
142
|
-
|
|
179
|
+
const source = {
|
|
143
180
|
gitUrl: ref.rawUrl,
|
|
144
181
|
ref: ref.version,
|
|
145
182
|
label: ref.rawUrl,
|
|
146
183
|
};
|
|
184
|
+
const token = await resolvePackageToken(options.tokenInput);
|
|
185
|
+
return this.withToken(source, token);
|
|
186
|
+
}
|
|
187
|
+
withToken(source, token) {
|
|
188
|
+
if (/^https?:\/\//.test(source.gitUrl)) {
|
|
189
|
+
const url = new URL(source.gitUrl);
|
|
190
|
+
if (url.username || url.password)
|
|
191
|
+
throw new Error('Credentials in Git URLs are refused; use --token-stdin, --token-file, --token-store or --token-env.');
|
|
192
|
+
}
|
|
193
|
+
if (token)
|
|
194
|
+
credentials.set(source, token);
|
|
195
|
+
return source;
|
|
147
196
|
}
|
|
148
197
|
async fetch(source, options = {}) {
|
|
198
|
+
this.withToken(source);
|
|
199
|
+
const token = credentials.get(source) ?? (hasTokenSource(options.tokenInput ?? {})
|
|
200
|
+
? await resolvePackageToken(options.tokenInput) : undefined);
|
|
201
|
+
let privateDir;
|
|
202
|
+
try {
|
|
203
|
+
let authentication;
|
|
204
|
+
if (token) {
|
|
205
|
+
if (process.platform === 'win32')
|
|
206
|
+
throw new Error('Git token authentication requires private temporary-file ACL validation on Windows; use SSH-agent authentication.');
|
|
207
|
+
const url = new URL(source.gitUrl);
|
|
208
|
+
if (url.protocol !== 'https:')
|
|
209
|
+
throw new Error('Git token authentication requires HTTPS.');
|
|
210
|
+
privateDir = await mkdtemp(join(tmpdir(), 'aiwg-git-credentials-'));
|
|
211
|
+
const config = join(privateDir, 'config');
|
|
212
|
+
// A path-only include survives NODE_DEBUG=child_process safely. The
|
|
213
|
+
// origin-scoped header is transient and never saved in .git/config.
|
|
214
|
+
await writeFile(config, `[http ${JSON.stringify(`${url.origin}/`)}]\n\textraHeader = ${JSON.stringify(`Authorization: Basic ${basicCredential(token)}`)}\n`, { mode: 0o600 });
|
|
215
|
+
authentication = { config, token };
|
|
216
|
+
}
|
|
217
|
+
return await this.fetchAuthenticated(source, options, authentication);
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
if (privateDir)
|
|
221
|
+
await rm(privateDir, { recursive: true, force: true });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async fetchAuthenticated(source, options, authentication) {
|
|
149
225
|
// Resolve discovery inputs before any deployment and cache by immutable
|
|
150
226
|
// commit rather than by a movable tag/branch name (#2009).
|
|
151
227
|
let requestedRef = source.ref;
|
|
152
228
|
if (!requestedRef) {
|
|
153
|
-
const latest = await resolveLatestTag(source.gitUrl);
|
|
229
|
+
const latest = await resolveLatestTag(source.gitUrl, authentication);
|
|
154
230
|
requestedRef = latest === 'latest' ? 'HEAD' : latest;
|
|
155
231
|
source.ref = requestedRef;
|
|
156
232
|
}
|
|
157
|
-
const advertisedCommit = await resolveRemoteCommit(source.gitUrl, requestedRef);
|
|
233
|
+
const advertisedCommit = await resolveRemoteCommit(source.gitUrl, requestedRef, authentication);
|
|
158
234
|
const { owner, name } = urlCacheIdentity(source.gitUrl);
|
|
159
235
|
if (advertisedCommit) {
|
|
160
236
|
const existing = buildCachePath(owner, name, advertisedCommit);
|
|
161
237
|
if (!options.refresh && existsSync(join(existing, '.git'))) {
|
|
162
|
-
const existingCommit = await git(['rev-parse', 'HEAD^{commit}'], existing);
|
|
238
|
+
const existingCommit = await git(['rev-parse', 'HEAD^{commit}'], existing, authentication);
|
|
163
239
|
if (existingCommit !== advertisedCommit)
|
|
164
240
|
throw new Error(`Immutable package cache mismatch at ${existing}`);
|
|
165
241
|
return existing;
|
|
@@ -169,9 +245,9 @@ export class GitAdapter {
|
|
|
169
245
|
await mkdir(ownerDir, { recursive: true });
|
|
170
246
|
const stage = await mkdtemp(join(ownerDir, `.${name}.resolve-`));
|
|
171
247
|
try {
|
|
172
|
-
await git(['clone', '--no-checkout', source.gitUrl, stage]);
|
|
173
|
-
await git(['checkout', '--detach', requestedRef], stage);
|
|
174
|
-
const resolvedCommit = await git(['rev-parse', 'HEAD^{commit}'], stage);
|
|
248
|
+
await git(['clone', '--no-checkout', source.gitUrl, stage], undefined, authentication);
|
|
249
|
+
await git(['checkout', '--detach', requestedRef], stage, authentication);
|
|
250
|
+
const resolvedCommit = await git(['rev-parse', 'HEAD^{commit}'], stage, authentication);
|
|
175
251
|
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(resolvedCommit)) {
|
|
176
252
|
throw new Error(`Git resolved '${requestedRef}' to invalid commit '${resolvedCommit}'`);
|
|
177
253
|
}
|
|
@@ -180,7 +256,7 @@ export class GitAdapter {
|
|
|
180
256
|
}
|
|
181
257
|
const cachePath = buildCachePath(owner, name, resolvedCommit);
|
|
182
258
|
if (existsSync(join(cachePath, '.git'))) {
|
|
183
|
-
const cachedCommit = await git(['rev-parse', 'HEAD^{commit}'], cachePath);
|
|
259
|
+
const cachedCommit = await git(['rev-parse', 'HEAD^{commit}'], cachePath, authentication);
|
|
184
260
|
if (cachedCommit !== resolvedCommit)
|
|
185
261
|
throw new Error(`Immutable package cache mismatch at ${cachePath}`);
|
|
186
262
|
await rm(stage, { recursive: true, force: true });
|
|
@@ -11,36 +11,17 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Authentication:
|
|
13
13
|
* - SSH clone: uses default SSH agent (recommended for teams)
|
|
14
|
-
* - HTTPS clone: reads token from GITEA_TOKEN env or ~/.config/gitea/token
|
|
14
|
+
* - HTTPS clone: resolves explicit sources, otherwise reads token from GITEA_TOKEN env or ~/.config/gitea/token
|
|
15
15
|
*
|
|
16
16
|
* @implements #557
|
|
17
17
|
*/
|
|
18
|
-
import { existsSync } from 'fs';
|
|
19
|
-
import { readFile } from 'fs/promises';
|
|
20
18
|
import { join } from 'path';
|
|
21
19
|
import { homedir } from 'os';
|
|
22
|
-
import { GitAdapter } from './git.js';
|
|
20
|
+
import { GitAdapter, resolvePackageToken } from './git.js';
|
|
23
21
|
/**
|
|
24
22
|
* Regex for "owner/name" or "owner/name@version" (no scheme prefix)
|
|
25
23
|
*/
|
|
26
24
|
const GITEA_SHORTHAND = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+(@[a-zA-Z0-9._/-]+)?$/;
|
|
27
|
-
/**
|
|
28
|
-
* Read Gitea token from standard file path (same as global CLAUDE.md convention)
|
|
29
|
-
*/
|
|
30
|
-
async function readGiteaToken() {
|
|
31
|
-
if (process.env.GITEA_TOKEN)
|
|
32
|
-
return process.env.GITEA_TOKEN;
|
|
33
|
-
const tokenPaths = [
|
|
34
|
-
join(homedir(), '.config', 'gitea', 'token'),
|
|
35
|
-
join(homedir(), '.aiwg', 'credentials', 'gitea'),
|
|
36
|
-
];
|
|
37
|
-
for (const p of tokenPaths) {
|
|
38
|
-
if (existsSync(p)) {
|
|
39
|
-
return (await readFile(p, 'utf-8')).trim();
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
25
|
/**
|
|
45
26
|
* Resolve the Gitea host
|
|
46
27
|
*/
|
|
@@ -66,25 +47,25 @@ export class GiteaAdapter extends GitAdapter {
|
|
|
66
47
|
return false; // has scheme prefix → not gitea shorthand
|
|
67
48
|
return GITEA_SHORTHAND.test(ref);
|
|
68
49
|
}
|
|
69
|
-
async resolve(ref) {
|
|
50
|
+
async resolve(ref, options = {}) {
|
|
70
51
|
if (!ref.owner || !ref.name)
|
|
71
52
|
return null;
|
|
72
53
|
const host = resolveGiteaHost();
|
|
73
|
-
const token = await
|
|
54
|
+
const token = await resolvePackageToken(options.tokenInput, 'GITEA_TOKEN', [join(homedir(), '.config', 'gitea', 'token'), join(homedir(), '.aiwg', 'credentials', 'gitea')]);
|
|
74
55
|
let gitUrl;
|
|
75
56
|
if (token) {
|
|
76
|
-
// HTTPS
|
|
77
|
-
gitUrl = `https://${
|
|
57
|
+
// HTTPS; credential is held outside the serializable source
|
|
58
|
+
gitUrl = `https://${host}/${ref.owner}/${ref.name}.git`;
|
|
78
59
|
}
|
|
79
60
|
else {
|
|
80
61
|
// SSH (relies on SSH agent)
|
|
81
62
|
gitUrl = `git@${host}:${ref.owner}/${ref.name}.git`;
|
|
82
63
|
}
|
|
83
|
-
return {
|
|
64
|
+
return this.withToken({
|
|
84
65
|
gitUrl,
|
|
85
66
|
ref: ref.version,
|
|
86
67
|
label: `${host}/${ref.owner}/${ref.name}${ref.version ? `@${ref.version}` : ''}`,
|
|
87
|
-
};
|
|
68
|
+
}, token);
|
|
88
69
|
}
|
|
89
70
|
async fetch(source, options = {}) {
|
|
90
71
|
return super.fetch(source, options);
|
|
@@ -5,32 +5,14 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Authentication:
|
|
7
7
|
* - SSH clone: uses default SSH agent
|
|
8
|
-
* - HTTPS clone: reads token from GITHUB_TOKEN env or ~/.aiwg/credentials/github
|
|
8
|
+
* - HTTPS clone: resolves explicit sources, otherwise reads token from GITHUB_TOKEN env or ~/.aiwg/credentials/github
|
|
9
9
|
*
|
|
10
10
|
* @implements #557
|
|
11
11
|
*/
|
|
12
|
-
import { existsSync } from 'fs';
|
|
13
|
-
import { readFile } from 'fs/promises';
|
|
14
12
|
import { join } from 'path';
|
|
15
13
|
import { homedir } from 'os';
|
|
16
|
-
import { GitAdapter } from './git.js';
|
|
14
|
+
import { GitAdapter, resolvePackageToken } from './git.js';
|
|
17
15
|
const GITHUB_HOST = 'github.com';
|
|
18
|
-
/**
|
|
19
|
-
* Read GitHub token from standard locations
|
|
20
|
-
*/
|
|
21
|
-
async function readGitHubToken() {
|
|
22
|
-
if (process.env.GITHUB_TOKEN)
|
|
23
|
-
return process.env.GITHUB_TOKEN;
|
|
24
|
-
const tokenPaths = [
|
|
25
|
-
join(homedir(), '.aiwg', 'credentials', 'github'),
|
|
26
|
-
];
|
|
27
|
-
for (const p of tokenPaths) {
|
|
28
|
-
if (existsSync(p)) {
|
|
29
|
-
return (await readFile(p, 'utf-8')).trim();
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
16
|
/**
|
|
35
17
|
* GitHubAdapter
|
|
36
18
|
*
|
|
@@ -45,22 +27,22 @@ export class GitHubAdapter extends GitAdapter {
|
|
|
45
27
|
canResolve(ref) {
|
|
46
28
|
return ref.startsWith('github:');
|
|
47
29
|
}
|
|
48
|
-
async resolve(ref) {
|
|
30
|
+
async resolve(ref, options = {}) {
|
|
49
31
|
if (!ref.owner || !ref.name)
|
|
50
32
|
return null;
|
|
51
|
-
const token = await
|
|
33
|
+
const token = await resolvePackageToken(options.tokenInput, 'GITHUB_TOKEN', [join(homedir(), '.aiwg', 'credentials', 'github')]);
|
|
52
34
|
let gitUrl;
|
|
53
35
|
if (token) {
|
|
54
|
-
gitUrl = `https://${
|
|
36
|
+
gitUrl = `https://${GITHUB_HOST}/${ref.owner}/${ref.name}.git`;
|
|
55
37
|
}
|
|
56
38
|
else {
|
|
57
39
|
gitUrl = `git@${GITHUB_HOST}:${ref.owner}/${ref.name}.git`;
|
|
58
40
|
}
|
|
59
|
-
return {
|
|
41
|
+
return this.withToken({
|
|
60
42
|
gitUrl,
|
|
61
43
|
ref: ref.version,
|
|
62
44
|
label: `github.com/${ref.owner}/${ref.name}${ref.version ? `@${ref.version}` : ''}`,
|
|
63
|
-
};
|
|
45
|
+
}, token);
|
|
64
46
|
}
|
|
65
47
|
async fetch(source, options = {}) {
|
|
66
48
|
return super.fetch(source, options);
|
|
@@ -99,11 +99,11 @@ export function parseRef(raw) {
|
|
|
99
99
|
/**
|
|
100
100
|
* Resolve a ref to a PackageSource using the appropriate adapter
|
|
101
101
|
*/
|
|
102
|
-
export async function resolveRef(ref) {
|
|
102
|
+
export async function resolveRef(ref, options = {}) {
|
|
103
103
|
for (const adapter of ALL_ADAPTERS) {
|
|
104
104
|
if (!adapter.canResolve(ref.raw))
|
|
105
105
|
continue;
|
|
106
|
-
const source = await adapter.resolve(ref);
|
|
106
|
+
const source = await adapter.resolve(ref, options);
|
|
107
107
|
if (source)
|
|
108
108
|
return { source, adapter };
|
|
109
109
|
}
|
|
@@ -171,7 +171,7 @@ export async function readPackageNamespace(cachePath, registryKey) {
|
|
|
171
171
|
*/
|
|
172
172
|
export async function installPackage(rawRef, options = {}) {
|
|
173
173
|
const ref = parseRef(rawRef);
|
|
174
|
-
const resolved = await resolveRef(ref);
|
|
174
|
+
const resolved = await resolveRef(ref, options);
|
|
175
175
|
if (!resolved) {
|
|
176
176
|
throw new Error(`Cannot resolve package reference: '${rawRef}'\n` +
|
|
177
177
|
`Supported formats:\n` +
|
|
@@ -185,7 +185,7 @@ export async function installPackage(rawRef, options = {}) {
|
|
|
185
185
|
const { source, adapter } = resolved;
|
|
186
186
|
if (options.ref)
|
|
187
187
|
source.ref = options.ref;
|
|
188
|
-
const cachePath = await adapter.fetch(source, { refresh: options.refresh });
|
|
188
|
+
const cachePath = await adapter.fetch(source, { refresh: options.refresh, tokenInput: options.tokenInput });
|
|
189
189
|
const discovered = await discoverInstallablePackage(cachePath, options.packageSelector);
|
|
190
190
|
const standardEnvelopePaths = [
|
|
191
191
|
join(cachePath, '.aiwg', 'marketplace', 'envelope.json'),
|
|
@@ -14,3 +14,8 @@ export function cliCommand(
|
|
|
14
14
|
pathExt?: string,
|
|
15
15
|
cwd?: string,
|
|
16
16
|
): [string, string[]];
|
|
17
|
+
|
|
18
|
+
export const HELPER_ENV_ALLOWLIST: readonly string[];
|
|
19
|
+
export const DESKTOP_HELPER_ENV_ALLOWLIST: readonly string[];
|
|
20
|
+
export const DEPLOYMENT_HELPER_ENV_ALLOWLIST: readonly string[];
|
|
21
|
+
export function helperEnvironment(source?: NodeJS.ProcessEnv, names?: readonly string[]): Record<string, string>;
|
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
// Like worker dispatch, helper processes receive only explicitly named state.
|
|
5
|
+
// Keep provider credentials, NODE_OPTIONS and unrelated shell variables out of
|
|
6
|
+
// process launch diagnostics as well as the child's own environment.
|
|
7
|
+
export const HELPER_ENV_ALLOWLIST = Object.freeze([
|
|
8
|
+
'PATH', 'PATHEXT', 'SYSTEMROOT', 'SYSTEMDRIVE', 'WINDIR', 'COMSPEC',
|
|
9
|
+
'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH',
|
|
10
|
+
'TMPDIR', 'TMP', 'TEMP', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TERM',
|
|
11
|
+
// Platform and network plumbing a child may need to reach anything at all
|
|
12
|
+
// (corporate proxies and CA bundles, Windows per-user data, git global config).
|
|
13
|
+
'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA',
|
|
14
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
|
|
15
|
+
'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'SSL_CERT_DIR',
|
|
16
|
+
'GIT_CONFIG_GLOBAL', 'GIT_CONFIG_SYSTEM', 'GIT_CONFIG_NOSYSTEM',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export const DESKTOP_HELPER_ENV_ALLOWLIST = Object.freeze([
|
|
20
|
+
'DISPLAY', 'WAYLAND_DISPLAY', 'XAUTHORITY', 'XDG_RUNTIME_DIR',
|
|
21
|
+
'DBUS_SESSION_BUS_ADDRESS', 'XDG_CURRENT_DESKTOP',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
// Deployment helpers read project/configuration locations and policy switches;
|
|
25
|
+
// none consumes provider authentication or the caller's complete shell state.
|
|
26
|
+
export const DEPLOYMENT_HELPER_ENV_ALLOWLIST = Object.freeze([
|
|
27
|
+
...HELPER_ENV_ALLOWLIST, 'AIWG_ROOT', 'AIWG_HOME', 'AIWG_CONFIG', 'AIWG_PROJECT_DIR',
|
|
28
|
+
'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'LOCALAPPDATA',
|
|
29
|
+
'AIWG_ARTIFACTS_PATH', 'AIWG_PROJECT_ARTIFACTS_PATH', 'AIWG_PROJECT_AIWG_DIR',
|
|
30
|
+
'AIWG_PROJECT_MEMORY_HOME', 'AIWG_USER_REGISTRY_PATH', 'AIWG_RULE_BINDING_INDEX_CACHE',
|
|
31
|
+
'AIWG_DEPLOY_SUBPROCESS', 'AIWG_REPORT_DEPLOY_COLLISIONS', 'CODEX_HOME', 'HERMES_HOME',
|
|
32
|
+
'OPENCLAW_PROFILE', 'OMP_PROFILE', 'PI_PROFILE', 'PI_CONFIG_DIR', 'PI_CODING_AGENT_DIR',
|
|
33
|
+
'AIWG_RESOLVE_IMPL_FROM', 'NODE_ENV', 'VITEST', 'NO_UPDATE_NOTIFIER', 'NO_COLOR', 'CI',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/** Copy only the helper's declared variable names; never merge the parent. */
|
|
37
|
+
export function helperEnvironment(source = process.env, names = HELPER_ENV_ALLOWLIST) {
|
|
38
|
+
const environment = {};
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
const value = source?.[name];
|
|
41
|
+
if (typeof value === 'string' && !value.includes('\0')) environment[name] = value;
|
|
42
|
+
}
|
|
43
|
+
return environment;
|
|
44
|
+
}
|
|
45
|
+
|
|
4
46
|
// Share PATH/PATHEXT lookup with callers that can launch arbitrary batch files.
|
|
5
47
|
function* executableCandidates(command, searchPath, pathExt, preferNative = false, cwd = process.cwd()) {
|
|
6
48
|
const candidates = path.isAbsolute(command)
|
|
@@ -640,6 +640,16 @@ export function workerInvocationDiagnostic(error, pointer, fallback = 'OG_CONFIG
|
|
|
640
640
|
if (error instanceof WorkerInvocationCheckError)
|
|
641
641
|
return { code: error.code, reason: error.reason, ...invocationChecks[error.reason] };
|
|
642
642
|
const code = error instanceof WorkerPolicyError ? error.code : fallback;
|
|
643
|
+
const signatureReason = error instanceof WorkerPolicyError
|
|
644
|
+
? error.details?.rejections.find(row => row.reason.startsWith('signature-'))?.reason : undefined;
|
|
645
|
+
if (code === 'OG_ADAPTER_UNQUALIFIED' && signatureReason) {
|
|
646
|
+
const messages = {
|
|
647
|
+
'signature-missing': 'Unsigned worker installation approval is unqualified. This requires deliberate one-time requalification: re-run aiwg doctor --probe-workers --qualify.',
|
|
648
|
+
'signature-trust-unavailable': 'Worker qualification signing trust is unavailable. Check effects.keyProvider in aiwg.config and the existing delivery effect-ledger keyring (aiwg effect keys list); restore trust, then re-run aiwg doctor --probe-workers --qualify.',
|
|
649
|
+
};
|
|
650
|
+
return { code, pointer, message: messages[signatureReason]
|
|
651
|
+
?? 'Worker installation signature is invalid or its keyid is untrusted. Re-run aiwg doctor --probe-workers --qualify.' };
|
|
652
|
+
}
|
|
643
653
|
const messages = {
|
|
644
654
|
OG_SANDBOX_EXTERNAL: "The CLI's own sandbox is disabled by explicit external policy; isolation relies on the per-task worktree and launcher resource scope.",
|
|
645
655
|
OG_NATIVE_SANDBOX_UNAVAILABLE: 'Native sandbox unavailable inside systemd user units; select policy launchMode native-sandbox or allow unprivileged user namespaces through host AppArmor policy.',
|
|
@@ -23,7 +23,7 @@ import * as fs from 'fs/promises';
|
|
|
23
23
|
import * as path from 'path';
|
|
24
24
|
import { buildProviderBootstrapBlock, PROVIDER_BOOTSTRAP_START, PROVIDER_BOOTSTRAP_END, dedupeProviderBootstrap, } from './workspace-context.js';
|
|
25
25
|
import { dominantLineEnding, withLineEnding } from './line-endings.js';
|
|
26
|
-
import { assertSafeContextFile } from './context-file-safety.js';
|
|
26
|
+
import { assertSafeContextFile, writeContextFileSync } from './context-file-safety.js';
|
|
27
27
|
export const CLAUDE_HOOK_START = '<!-- AIWG:claude-md-hook:start -->';
|
|
28
28
|
export const CLAUDE_HOOK_END = '<!-- AIWG:claude-md-hook:end -->';
|
|
29
29
|
export function buildClaudeHookBlock(_policy = {}) {
|
|
@@ -92,7 +92,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
92
92
|
catch (err) {
|
|
93
93
|
if (err.code === 'ENOENT') {
|
|
94
94
|
const minimal = `${block}\n`;
|
|
95
|
-
|
|
95
|
+
writeContextFileSync(claudeMdPath, minimal, 'utf8');
|
|
96
96
|
result.action = 'created';
|
|
97
97
|
return result;
|
|
98
98
|
}
|
|
@@ -121,7 +121,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
121
121
|
const updated = existing.substring(0, bootstrapStartIdx)
|
|
122
122
|
+ block
|
|
123
123
|
+ existing.substring(bootstrapEndIdx);
|
|
124
|
-
|
|
124
|
+
writeContextFileSync(claudeMdPath, dedupeProviderBootstrap(updated), 'utf8');
|
|
125
125
|
result.action = 'updated';
|
|
126
126
|
return result;
|
|
127
127
|
}
|
|
@@ -132,7 +132,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
132
132
|
// Ensure the file ends with a single newline before appending.
|
|
133
133
|
const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
|
|
134
134
|
const updated = `${trimmed}${lineEnding}${block}${lineEnding}`;
|
|
135
|
-
|
|
135
|
+
writeContextFileSync(claudeMdPath, dedupeProviderBootstrap(updated), 'utf8');
|
|
136
136
|
result.action = 'inserted';
|
|
137
137
|
return result;
|
|
138
138
|
}
|
|
@@ -145,12 +145,12 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
145
145
|
// With --force, back up and append a fresh block.
|
|
146
146
|
if (opts.detectExistingFiles !== false) {
|
|
147
147
|
const backupPath = `${claudeMdPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
148
|
-
|
|
148
|
+
writeContextFileSync(backupPath, existing, 'utf8');
|
|
149
149
|
result.backupPath = backupPath;
|
|
150
150
|
}
|
|
151
151
|
const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
|
|
152
152
|
const updated = `${trimmed}${lineEnding}${block}${lineEnding}`;
|
|
153
|
-
|
|
153
|
+
writeContextFileSync(claudeMdPath, dedupeProviderBootstrap(updated), 'utf8');
|
|
154
154
|
result.action = 'inserted';
|
|
155
155
|
return result;
|
|
156
156
|
}
|
|
@@ -159,7 +159,7 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
159
159
|
const currentBlock = existing.substring(startIdx, blockEndOffset);
|
|
160
160
|
if (currentBlock === block) {
|
|
161
161
|
if (repaired) {
|
|
162
|
-
|
|
162
|
+
writeContextFileSync(claudeMdPath, existing, 'utf8');
|
|
163
163
|
result.action = 'updated';
|
|
164
164
|
return result;
|
|
165
165
|
}
|
|
@@ -171,14 +171,14 @@ export async function ensureClaudeMdHook(projectPath, opts = {}) {
|
|
|
171
171
|
// the case where an operator made deliberate edits inside the block).
|
|
172
172
|
if (opts.force) {
|
|
173
173
|
const backupPath = `${claudeMdPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
174
|
-
|
|
174
|
+
writeContextFileSync(backupPath, existing, 'utf8');
|
|
175
175
|
result.backupPath = backupPath;
|
|
176
176
|
}
|
|
177
177
|
else {
|
|
178
178
|
result.warnings.push(`CLAUDE.md AIWG hook block has been modified inside the markers; updating to canonical content. Operator content outside the block is preserved.`);
|
|
179
179
|
}
|
|
180
180
|
const updated = existing.substring(0, startIdx) + block + existing.substring(blockEndOffset);
|
|
181
|
-
|
|
181
|
+
writeContextFileSync(claudeMdPath, dedupeProviderBootstrap(updated), 'utf8');
|
|
182
182
|
result.action = 'updated';
|
|
183
183
|
return result;
|
|
184
184
|
}
|