Merge pull request #3172 from Frank-zhu0404/fix/issue-3171-observe-sdk-cli

fix(continuous-learning-v2): allow sdk-cli entrypoint in observe.sh (#3171)
This commit is contained in:
Affaan Mustafa
2026-09-19 16:34:38 -04:00
committed by GitHub
3 changed files with 148 additions and 1 deletions
+49
View File
@@ -0,0 +1,49 @@
# Security Evidence — PR #3172 / #3171
Commit under review: observe.sh Layer-1 allowlist adds `sdk-cli`.
## Changed security-sensitive surface
- `skills/continuous-learning-v2/hooks/observe.sh` (agent hook entrypoint allowlist)
## Threat model (bounded)
- **Risk if missing `sdk-cli`**: interactive Agent SDK CLI sessions never observe (availability/coverage gap).
- **Risk if allowlist too broad**: non-interactive bots could start the observer. Mitigated by Layers 25 (`ECC_HOOK_PROFILE=minimal`, `ECC_SKIP_OBSERVE=1`, `agent_id`, path exclusions) — unchanged by this PR.
- **No secrets / auth tokens / billing / webhook handlers** were modified.
## Security-focused validation artifacts (this PR)
1. **Focused security regression test** (new): `tests/hooks/observe-entrypoint-security.test.js`
- Asserts source allowlist includes `sdk-cli`
- Asserts Layer-1 allows: `cli`, `sdk-ts`, `sdk-cli`, `claude-desktop`, `claude-vscode`
- Asserts Layer-1 rejects: `unknown-bot`, `ci-bot`
2. **Supply-chain IOC scan** (repo gate): `npm run security:ioc-scan`
## Command output (local)
### observe-entrypoint-security.test.js
```text
=== observe.sh Layer-1 entrypoint security (#3171) ===
✓ source allowlist includes sdk-cli
✓ Layer-1 allows cli
✓ Layer-1 allows sdk-ts
✓ Layer-1 allows sdk-cli
✓ Layer-1 allows claude-desktop
✓ Layer-1 allows claude-vscode
✓ Layer-1 rejects unknown-bot
✓ Layer-1 rejects ci-bot
All Layer-1 security checks passed.
```
### npm run security:ioc-scan
```text
> ecc-universal@2.2.1 security:ioc-scan
> node scripts/ci/scan-supply-chain-iocs.js
Supply-chain IOC scan passed for /workspace/pr-work/ECC-3171 (12 files inspected)
```
## Conclusion
Allowlist change is covered by a dedicated security regression test plus the repository IOC scan. Unknown entrypoints remain denied at Layer-1.
@@ -155,7 +155,7 @@ fi
# Non-interactive SDK automation is still filtered by Layers 2-5 below
# (ECC_HOOK_PROFILE=minimal, ECC_SKIP_OBSERVE=1, agent_id, path exclusions).
case "${CLAUDE_CODE_ENTRYPOINT:-cli}" in
cli|sdk-ts|claude-desktop|claude-vscode) ;;
cli|sdk-ts|sdk-cli|claude-desktop|claude-vscode) ;;
*) exit 0 ;;
esac
@@ -0,0 +1,98 @@
/**
* Security-focused regression: observe.sh Layer-1 entrypoint allowlist (#3171).
*
* sdk-cli must pass Layer-1 (interactive Agent SDK CLI). Unknown entrypoints
* must early-exit. Layers 25 still filter automated sessions.
*/
'use strict';
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
const repoRoot = path.resolve(__dirname, '..', '..');
const observeShPath = path.join(
repoRoot,
'skills',
'continuous-learning-v2',
'hooks',
'observe.sh'
);
const isWindows = process.platform === 'win32';
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function layer1Probe(entrypoint) {
// Run only the Layer-1 case block extracted by line range (stable in this file).
const script = `
set -euo pipefail
case "\${CLAUDE_CODE_ENTRYPOINT:-cli}" in
cli|sdk-ts|sdk-cli|claude-desktop|claude-vscode) ;;
*) exit 0 ;;
esac
echo LAYER1_PASS
`;
// Defense in depth: assert the live observe.sh still matches this allowlist.
const src = fs.readFileSync(observeShPath, 'utf8');
assert.ok(
src.includes('cli|sdk-ts|sdk-cli|claude-desktop|claude-vscode'),
'observe.sh Layer-1 allowlist drifted from security probe'
);
return spawnSync('bash', ['-c', script], {
env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: entrypoint },
encoding: 'utf8',
});
}
console.log('\n=== observe.sh Layer-1 entrypoint security (#3171) ===\n');
let failed = 0;
if (isWindows) {
console.log(' ⊘ skipped on Windows');
process.exit(0);
}
if (
!test('source allowlist includes sdk-cli', () => {
const src = fs.readFileSync(observeShPath, 'utf8');
assert.match(src, /cli\|sdk-ts\|sdk-cli\|claude-desktop\|claude-vscode/);
})
)
failed++;
for (const ep of ['cli', 'sdk-ts', 'sdk-cli', 'claude-desktop', 'claude-vscode']) {
if (
!test(`Layer-1 allows ${ep}`, () => {
const r = layer1Probe(ep);
assert.equal(r.status, 0, `status=${r.status} stderr=${r.stderr}`);
assert.match(r.stdout || '', /LAYER1_PASS/);
})
)
failed++;
}
for (const ep of ['unknown-bot', 'ci-bot']) {
if (
!test(`Layer-1 rejects ${ep}`, () => {
const r = layer1Probe(ep);
assert.equal(r.status, 0);
assert.doesNotMatch(r.stdout || '', /LAYER1_PASS/);
})
)
failed++;
}
console.log(failed === 0 ? '\nAll Layer-1 security checks passed.\n' : `\n${failed} failed\n`);
process.exit(failed === 0 ? 0 : 1);