fix(harness): an env or config start-hook opt-in with an unsafe plugin folder turns the hook off instead of failing refresh (GST-718)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-11-01 06:18:22 +00:00
1 parent ef00f3ad88
commit 2ec30464ff
3 files changed
+14 -4

No files matched your search

+2 -2
View File
@@ -134,7 +134,7 @@ async function main(): Promise<number> {
}
case 'refresh': {
let code = 0;
for (const r of refresh(base)) { if (r.status !== 'unchanged' || r.hook?.required || r.hook?.retried) receipt(r); code ||= hookFailed(r); }
for (const r of refresh(base)) { if (r.status !== 'unchanged' || r.hook?.required || r.hook?.retried || r.hook?.detail) receipt(r); code ||= hookFailed(r); }
return code;
}
case 'auto': {
@@ -145,7 +145,7 @@ async function main(): Promise<number> {
try { const r = register({ ...base, target: TARGETS[n], registry: true }); done.add(r.id); receipt(r); hookFailed(r); }
catch (e) { if (e instanceof HarnessError) { console.error(e.report()); failed = 1; } else throw e; }
}
try { for (const r of refresh(base, done)) { if (r.status !== 'unchanged' || r.hook?.required || r.hook?.retried) receipt(r); hookFailed(r); } }
try { for (const r of refresh(base, done)) { if (r.status !== 'unchanged' || r.hook?.required || r.hook?.retried || r.hook?.detail) receipt(r); hookFailed(r); } }
catch (e) { if (e instanceof HarnessError) { console.error(e.report()); failed = 1; } else throw e; }
for (const n of d.present.filter(n => !d.inside.includes(n) && TARGETS[n].mechanism === 'skill-folder' && !listLedgers(stateRoot).some(l => l.ledger.meta.target === n))) {
console.log('gstack-harness: ' + TARGETS[n].displayName + ' is installed here; to give it gstack skills: ' + self + ' register ' + n);
+6 -1
View File
@@ -547,7 +547,12 @@ function hookPlan(opts: RegisterOptions, dest: string, prev: Ledger | null): Hoo
if (plan.explicit) throw new HarnessError('HARNESS_HOOK_UNSUPPORTED', 'the start hook goes into the ' + t.displayName + ' profile ' + profile + ', and ' + why);
return { ...plan, on: false, note: 'off: ' + why + ' (' + choice.source + ' asked for the hook)' };
}
assertHookRoot(t.name, profile, root, prevOn ? prev!.meta.hook_root : undefined, (code, msg) => { throw new HarnessError(code, msg); });
try {
assertHookRoot(t.name, profile, root, prevOn ? prev!.meta.hook_root : undefined, (code, msg) => { throw new HarnessError(code, msg); });
} catch (e) {
if (plan.explicit || prevOn || !(e instanceof HarnessError)) throw e;
return { ...plan, on: false, note: 'off: ' + e.message + ' (' + choice.source + ' asked for the hook; ' + HARNESS_CODES[e.code].fix + ')' };
}
return { ...plan, files: hookFiles(t.name, root) };
}
+6 -1
View File
@@ -149,7 +149,7 @@ describe('start hook: who decides', () => {
expect(fs.existsSync(path.join(s.d, 'custom'))).toBe(false);
});
test('a plugin folder gstack did not write, or a symlink on the way to it, stops the hook untouched', () => {
test('a plugin folder gstack did not write, or a symlink on the way to it, stops an explicit --start-hook with nothing written, and turns an env or config opt-in into hook off', () => {
const r = rig('openclaw');
fs.mkdirSync(r.root, { recursive: true });
fs.writeFileSync(path.join(r.root, 'index.js'), 'mine');
@@ -160,6 +160,11 @@ describe('start hook: who decides', () => {
fs.symlinkSync(path.join(s.d, 'elsewhere'), path.join(s.profile, 'plugins'));
expect(() => register(s.o({ startHook: true }))).toThrow(expect.objectContaining({ code: 'HARNESS_UNSAFE_DESTINATION' }));
expect(fs.readdirSync(path.join(s.d, 'elsewhere'))).toEqual([]);
const viaEnv = register({ ...s.o(), env: { ...s.env, GSTACK_HARNESS_START_HOOK: '1' } });
expect(viaEnv.hook?.state).toBe('off');
expect(viaEnv.hook?.detail).toContain('is a symlink (env asked for the hook');
expect(fs.existsSync(path.join(s.profile, 'skills', 'gstack'))).toBe(true);
expect(fs.readdirSync(path.join(s.d, 'elsewhere'))).toEqual([]);
expect(r.calls().concat(s.calls())).toEqual([]);
});
});