leerness 1.9.149 → 1.9.150
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/CHANGELOG.md +43 -0
- package/README.md +3 -3
- package/bin/harness.js +143 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.150 — 2026-05-20
|
|
4
|
+
|
|
5
|
+
**Sandboxing — `runCommandSafe()` wrapper + REPL slash-commands (3중 LLM 합의 #3 / Codex 권고).**
|
|
6
|
+
|
|
7
|
+
자율 모드 80 라운드 마일스톤. 1.9.149 REPL 위에 **샌드박스 보안 레이어** + **leerness 내부 명령 직접 호출**을 추가.
|
|
8
|
+
|
|
9
|
+
### Added — `runCommandSafe(cmd, args, opts)` sandbox wrapper
|
|
10
|
+
- **cwd jail** — `cwd` 가 root 밖 (path traversal) 이면 즉시 exit 126 + `blocked: 'cwd_jail'` 기록
|
|
11
|
+
- **shell:false 기본** — shell injection 표면 차단. `allowShell: true` 시만 shell:true (npm/pytest 호환)
|
|
12
|
+
- **env scrub** — 안전 화이트리스트만 통과 (`PATH`, `HOME`, `TMP`, `LEERNESS_*`, `NPM_CONFIG_*`, ...)
|
|
13
|
+
- 시크릿 환경변수 (DB_PASSWORD, API_KEY 등) 자식 프로세스에 누출 방지
|
|
14
|
+
- **timeout 한도** — 기본 5분, max 10분 (clamp)
|
|
15
|
+
- **permissions 검증** — 1.9.146 `permissions.shell.allowList` 자동 연동
|
|
16
|
+
- basic 모드 (`shell.exec=false`) 에선 핵심 도구 (git/npm/node/pnpm/yarn) 만 허용
|
|
17
|
+
- allowList 외 명령은 즉시 reject + `blocked: 'permissions'` 기록
|
|
18
|
+
- **자동 observability** — 호출마다 `_recordRun` 으로 cmd/args/durationMs/status/cwd 자동 기록
|
|
19
|
+
|
|
20
|
+
### Changed — 위험 호출 sandbox 치환
|
|
21
|
+
- `verify-code` (line ~7473): `cp.spawnSync(t.cmd, [], { shell: true })` → `runCommandSafe(t.cmd, ...)`
|
|
22
|
+
- `deploy auto` (line ~10580): `cp.spawnSync(meta.deployCommand, [], { shell: true })` → `runCommandSafe(...)`
|
|
23
|
+
- `agents bench` (line ~8866): `cp.spawnSync(cmd, cliArgs, { shell: true })` → `runCommandSafe(...)`
|
|
24
|
+
- 3 곳 모두 env scrub + cwd jail + observability 자동 적용
|
|
25
|
+
|
|
26
|
+
### Added — REPL slash-commands (1.9.149 위에)
|
|
27
|
+
- `:verify` — `leerness verify-code` 직접 호출 (sandboxed)
|
|
28
|
+
- `:audit` — `leerness audit` (보안 + drift + lazy)
|
|
29
|
+
- `:handoff` — `leerness handoff --quiet --no-drift-check`
|
|
30
|
+
- `:health` — `leerness health --json`
|
|
31
|
+
- 모두 `runCommandSafe` 경유 — 자식 leerness 호출도 sandbox 적용
|
|
32
|
+
- REPL 안에서 agent가 "현재 상태 점검해줘" 같은 메타 명령으로 leerness 기능을 즉시 호출 가능
|
|
33
|
+
|
|
34
|
+
### Security
|
|
35
|
+
- 시크릿 환경변수 누출 표면 대폭 축소 — `runCommandSafe` 호출 시 화이트리스트 외 env 미전달
|
|
36
|
+
- 사용자 글로벌 룰 준수: API 키/DB 비밀번호 절대 자식 프로세스에 자동 전파 금지
|
|
37
|
+
- `_reports/`, `.harness/agent-sessions/`, `.harness/runs/`, `.harness/credentials.local.json` 비공개 정책 유지
|
|
38
|
+
|
|
39
|
+
### Verified
|
|
40
|
+
- e2e 220/220 (slash-commands handleMeta 통합 + sandbox wrapper)
|
|
41
|
+
- stress-v95: 19/19 (sandbox 5종 + REPL slash 4종 + 누적 회귀 1.9.146~149)
|
|
42
|
+
- VERSION = 1.9.150 / autonomous-rounds = 80
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
3
46
|
## 1.9.149 — 2026-05-20
|
|
4
47
|
|
|
5
48
|
**REPL agent (Hermes/OpenClaw/OpenCode 스타일) + observability lite — 사용자 명시 요청 + 3중 LLM 합의 #2.**
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **AI 코딩 에이전트의 거짓 완료·중복·망각·충돌을 막아주는 검수·기억·협업 CLI 하네스.**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/leerness) [](https://www.npmjs.com/package/leerness) []() []() []() []() []() []() []() []() []() []() []()
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
║ ██║ ██╔══╝ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══╝ ╚════██║ ║
|
|
13
13
|
║ ███████╗███████╗███████╗██║ ██║██║ ╚████║███████╗███████║ ║
|
|
14
14
|
║ ╚══════╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝ ║
|
|
15
|
-
║ v1.9.
|
|
16
|
-
║ verify · remember · orchestrate · audit ·
|
|
15
|
+
║ v1.9.150 AI Agent Reliability Harness + Sandbox ║
|
|
16
|
+
║ verify · remember · orchestrate · audit · sandbox · drift ║
|
|
17
17
|
╚══════════════════════════════════════════════════════════════╝
|
|
18
18
|
```
|
|
19
19
|
|
package/bin/harness.js
CHANGED
|
@@ -6,7 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const cp = require('child_process');
|
|
7
7
|
const readline = require('readline');
|
|
8
8
|
|
|
9
|
-
const VERSION = '1.9.
|
|
9
|
+
const VERSION = '1.9.150';
|
|
10
10
|
const MARK = '<!-- leerness:managed -->';
|
|
11
11
|
const README_START = '<!-- leerness:project-readme:start -->';
|
|
12
12
|
const README_END = '<!-- leerness:project-readme:end -->';
|
|
@@ -7470,7 +7470,8 @@ function verifyCodeCmd(root) {
|
|
|
7470
7470
|
for (const t of tasks) {
|
|
7471
7471
|
log(`\n## ${t.name}: ${t.cmd}`);
|
|
7472
7472
|
const start = Date.now();
|
|
7473
|
-
|
|
7473
|
+
// 1.9.150: runCommandSafe — cwd jail + env scrub + observability 자동 (shell:true 유지 — npm/pytest 호환)
|
|
7474
|
+
const r = runCommandSafe(t.cmd, [], { cwd: root, root, timeout: 5 * 60 * 1000, allowShell: true, kind: 'verify_code_task', label: `verify-${t.name}` });
|
|
7474
7475
|
const dur = Date.now() - start;
|
|
7475
7476
|
if (r.status === 0) ok(`${t.name} passed (${dur}ms)`);
|
|
7476
7477
|
else if (t.optional && r.status === 127) warn(`${t.name} 스킵 (${t.cmd} 없음)`);
|
|
@@ -8862,7 +8863,8 @@ async function _benchmarkMeasure(root, task) {
|
|
|
8862
8863
|
else if (agent.id === 'gemini') { cmd = 'gemini'; cliArgs = ['-p', task]; }
|
|
8863
8864
|
else continue;
|
|
8864
8865
|
const t0 = Date.now();
|
|
8865
|
-
|
|
8866
|
+
// 1.9.150: runCommandSafe — agent CLI bench sandbox (env scrub + observability)
|
|
8867
|
+
const r = runCommandSafe(cmd, cliArgs, { cwd: process.cwd(), root, timeout: 60000, allowShell: true, kind: 'agent_bench', label: `bench-${agent.id}`, allowOutsideCwd: true });
|
|
8866
8868
|
const baseTime = Date.now() - t0;
|
|
8867
8869
|
// leerness 검수 layer time 추정 (verify-claim 형식)
|
|
8868
8870
|
const t1 = Date.now();
|
|
@@ -10010,6 +10012,111 @@ function _recordRun(root, entry) {
|
|
|
10010
10012
|
return id;
|
|
10011
10013
|
} catch { return null; }
|
|
10012
10014
|
}
|
|
10015
|
+
|
|
10016
|
+
// 1.9.150: Sandboxing — runCommandSafe wrapper (Codex 권고: 3중 LLM 합의 #3)
|
|
10017
|
+
// cwd jail (root 밖 거부) + shell:false 기본 + timeout + env scrub + permissions allowList 검증 + _recordRun 자동
|
|
10018
|
+
const _ENV_SAFE_KEYS = new Set([
|
|
10019
|
+
'PATH', 'HOME', 'USERPROFILE', 'TEMP', 'TMP', 'TMPDIR', 'NODE_PATH', 'NODE_ENV',
|
|
10020
|
+
'LANG', 'LC_ALL', 'LC_CTYPE', 'SHELL', 'COMSPEC', 'SYSTEMROOT', 'WINDIR', 'OS',
|
|
10021
|
+
'PROCESSOR_ARCHITECTURE', 'PROCESSOR_IDENTIFIER', 'NUMBER_OF_PROCESSORS',
|
|
10022
|
+
'PROGRAMFILES', 'PROGRAMFILES(X86)', 'APPDATA', 'LOCALAPPDATA',
|
|
10023
|
+
'GITHUB_TOKEN', 'NPM_TOKEN', 'CI', 'GH_TOKEN'
|
|
10024
|
+
]);
|
|
10025
|
+
function _scrubEnv(extraEnv) {
|
|
10026
|
+
const out = {};
|
|
10027
|
+
for (const k of Object.keys(process.env || {})) {
|
|
10028
|
+
if (_ENV_SAFE_KEYS.has(k) || k.startsWith('LEERNESS_') || k.startsWith('NPM_CONFIG_')) {
|
|
10029
|
+
out[k] = process.env[k];
|
|
10030
|
+
}
|
|
10031
|
+
}
|
|
10032
|
+
if (extraEnv && typeof extraEnv === 'object') {
|
|
10033
|
+
for (const k of Object.keys(extraEnv)) {
|
|
10034
|
+
// Allow caller overrides — explicit opt-in
|
|
10035
|
+
if (extraEnv[k] !== undefined) out[k] = String(extraEnv[k]);
|
|
10036
|
+
}
|
|
10037
|
+
}
|
|
10038
|
+
return out;
|
|
10039
|
+
}
|
|
10040
|
+
function _isCwdSafe(root, cwd) {
|
|
10041
|
+
try {
|
|
10042
|
+
if (!cwd) return true;
|
|
10043
|
+
const r = path.resolve(absRoot(root));
|
|
10044
|
+
const c = path.resolve(cwd);
|
|
10045
|
+
if (c === r) return true;
|
|
10046
|
+
const rel = path.relative(r, c);
|
|
10047
|
+
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
10048
|
+
} catch { return false; }
|
|
10049
|
+
}
|
|
10050
|
+
function runCommandSafe(cmd, args, opts) {
|
|
10051
|
+
// opts: { cwd, root, timeout, env, stdio, kind, label, allowShell, encoding, input, allowOutsideCwd }
|
|
10052
|
+
opts = opts || {};
|
|
10053
|
+
const root = opts.root || opts.cwd || process.cwd();
|
|
10054
|
+
const cwd = opts.cwd || root;
|
|
10055
|
+
const cmdStr = String(cmd || '').trim();
|
|
10056
|
+
const argList = Array.isArray(args) ? args.slice() : [];
|
|
10057
|
+
const t0 = Date.now();
|
|
10058
|
+
const label = opts.label || opts.kind || 'shell_exec';
|
|
10059
|
+
// 1) cwd jail
|
|
10060
|
+
if (!opts.allowOutsideCwd && !_isCwdSafe(root, cwd)) {
|
|
10061
|
+
const r = { status: 126, stdout: '', stderr: `runCommandSafe: cwd outside root rejected (${cwd})`, error: 'cwd_jail', blocked: true };
|
|
10062
|
+
try { _recordRun(root, { kind: label, cmd: cmdStr, args: argList, durationMs: Date.now() - t0, ok: false, blocked: 'cwd_jail' }); } catch {}
|
|
10063
|
+
return r;
|
|
10064
|
+
}
|
|
10065
|
+
// 2) permissions allowList (1.9.146)
|
|
10066
|
+
try {
|
|
10067
|
+
const perms = _readPermissions(root);
|
|
10068
|
+
const exec = perms.shell?.exec !== false; // basic 에선 false
|
|
10069
|
+
const allow = perms.shell?.allowList || [];
|
|
10070
|
+
if (!exec && !opts.allowOutsideCwd) {
|
|
10071
|
+
// basic 모드 — git/npm/node 같은 핵심 도구는 허용 (release/install 흐름 유지)
|
|
10072
|
+
const coreAllow = ['git', 'npm', 'npx', 'node', 'pnpm', 'yarn'];
|
|
10073
|
+
const first = cmdStr.split(/\s+/)[0];
|
|
10074
|
+
if (!coreAllow.includes(first) && !allow.includes('*') && !allow.includes(first)) {
|
|
10075
|
+
const r = { status: 126, stdout: '', stderr: `runCommandSafe: shell.exec=false (mode=${perms.mode}). allowList: ${allow.join(',') || '(없음)'} / core: ${coreAllow.join(',')}`, error: 'permissions', blocked: true };
|
|
10076
|
+
try { _recordRun(root, { kind: label, cmd: cmdStr, args: argList, durationMs: Date.now() - t0, ok: false, blocked: 'permissions', mode: perms.mode }); } catch {}
|
|
10077
|
+
return r;
|
|
10078
|
+
}
|
|
10079
|
+
}
|
|
10080
|
+
} catch {}
|
|
10081
|
+
// 3) spawn — shell:false 기본 (shell injection 차단). allowShell=true 시만 shell:true (deploy/build 호환)
|
|
10082
|
+
const useShell = !!opts.allowShell;
|
|
10083
|
+
const timeout = Math.min(opts.timeout || 5 * 60 * 1000, 10 * 60 * 1000);
|
|
10084
|
+
const spawnOpts = {
|
|
10085
|
+
cwd,
|
|
10086
|
+
encoding: opts.encoding || 'utf8',
|
|
10087
|
+
timeout,
|
|
10088
|
+
shell: useShell,
|
|
10089
|
+
env: _scrubEnv(opts.env),
|
|
10090
|
+
input: opts.input,
|
|
10091
|
+
stdio: opts.stdio || 'pipe'
|
|
10092
|
+
};
|
|
10093
|
+
let r;
|
|
10094
|
+
try {
|
|
10095
|
+
if (useShell) {
|
|
10096
|
+
// shell:true 모드 — 인자가 cmd 안에 포함된 단일 문자열인 경우 처리
|
|
10097
|
+
r = cp.spawnSync(cmdStr + (argList.length ? ' ' + argList.join(' ') : ''), [], spawnOpts);
|
|
10098
|
+
} else {
|
|
10099
|
+
// 단일 명령어로 들어온 경우 자동 분리
|
|
10100
|
+
let bin = cmdStr, finalArgs = argList;
|
|
10101
|
+
if (!argList.length && /\s/.test(cmdStr)) {
|
|
10102
|
+
const parts = cmdStr.split(/\s+/);
|
|
10103
|
+
bin = parts[0]; finalArgs = parts.slice(1);
|
|
10104
|
+
}
|
|
10105
|
+
r = cp.spawnSync(bin, finalArgs, spawnOpts);
|
|
10106
|
+
}
|
|
10107
|
+
} catch (e) {
|
|
10108
|
+
r = { status: 1, stdout: '', stderr: e.message, error: 'spawn_exception' };
|
|
10109
|
+
}
|
|
10110
|
+
const dt = Date.now() - t0;
|
|
10111
|
+
try {
|
|
10112
|
+
_recordRun(root, {
|
|
10113
|
+
kind: label, cmd: cmdStr, args: argList,
|
|
10114
|
+
durationMs: dt, status: r.status, ok: r.status === 0,
|
|
10115
|
+
shell: useShell, cwd: path.relative(absRoot(root), cwd) || '.'
|
|
10116
|
+
});
|
|
10117
|
+
} catch {}
|
|
10118
|
+
return r;
|
|
10119
|
+
}
|
|
10013
10120
|
function runsListCmd(root) {
|
|
10014
10121
|
root = absRoot(root || process.cwd());
|
|
10015
10122
|
const dir = _runsDir(root);
|
|
@@ -10077,8 +10184,8 @@ async function _agentRepl(root, opts) {
|
|
|
10077
10184
|
// 환영 메시지 + 모델 선택
|
|
10078
10185
|
log('');
|
|
10079
10186
|
log(C.bold(C.cy(' ╔════════════════════════════════════════════════════╗')));
|
|
10080
|
-
log(C.bold(C.cy(' ║ leerness agent — REPL mode (1.9.
|
|
10081
|
-
log(C.bold(C.cy(' ║ Hermes / OpenClaw / OpenCode 스타일
|
|
10187
|
+
log(C.bold(C.cy(' ║ leerness agent — REPL mode (1.9.150) ║')));
|
|
10188
|
+
log(C.bold(C.cy(' ║ Hermes / OpenClaw / OpenCode 스타일 + Sandbox ║')));
|
|
10082
10189
|
log(C.bold(C.cy(' ╚════════════════════════════════════════════════════╝')));
|
|
10083
10190
|
log('');
|
|
10084
10191
|
// Ollama 모델 자동 감지 — model이 명시되지 않았으면 사용자에게 선택지 제공
|
|
@@ -10100,6 +10207,7 @@ async function _agentRepl(root, opts) {
|
|
|
10100
10207
|
}
|
|
10101
10208
|
log('');
|
|
10102
10209
|
log(C.dim(' 메타 명령: :help | :model <m> | :role <r> | :provider <p> | :clear | :save | :history | :quit'));
|
|
10210
|
+
log(C.dim(' Slash 명령 (1.9.150): :verify | :audit | :handoff | :health'));
|
|
10103
10211
|
log(C.dim(` 현재 — provider=${state.provider} model=${state.model || '(없음)'} role=${state.role} permissions=${_readPermissions(root).mode}`));
|
|
10104
10212
|
log('');
|
|
10105
10213
|
const prompt = () => isTty ? C.cy(`agent[${state.role}]> `) : 'agent> ';
|
|
@@ -10125,6 +10233,11 @@ async function _agentRepl(root, opts) {
|
|
|
10125
10233
|
log(' :save — 세션 즉시 저장');
|
|
10126
10234
|
log(' :permissions — 현재 권한 모드 표시');
|
|
10127
10235
|
log(' :quit / :exit / :q — 종료 (자동 저장)');
|
|
10236
|
+
log(C.bold('\n Slash 명령 (1.9.150) — leerness 내부 명령 직접 호출:'));
|
|
10237
|
+
log(' :verify — leerness verify-code (테스트/타입/린트 자동 검수)');
|
|
10238
|
+
log(' :audit — leerness audit (보안 + drift + lazy)');
|
|
10239
|
+
log(' :handoff — leerness handoff --quiet (현재 컨텍스트 요약)');
|
|
10240
|
+
log(' :health — leerness health --json (종합 헬스 체크)');
|
|
10128
10241
|
return false;
|
|
10129
10242
|
}
|
|
10130
10243
|
if (op === 'model') { state.model = rest.join(' ') || state.model; log(C.green(` model = ${state.model}`)); return false; }
|
|
@@ -10149,6 +10262,26 @@ async function _agentRepl(root, opts) {
|
|
|
10149
10262
|
}
|
|
10150
10263
|
if (op === 'save') { saveSession(); log(C.dim(` → ${rel(root, sessionPath())}`)); return false; }
|
|
10151
10264
|
if (op === 'permissions') { permissionsListCmd(root); return false; }
|
|
10265
|
+
// 1.9.150: leerness 내부 명령 slash-commands — :verify / :audit / :handoff / :health
|
|
10266
|
+
if (op === 'verify' || op === 'audit' || op === 'handoff' || op === 'health') {
|
|
10267
|
+
const subArgs = {
|
|
10268
|
+
verify: ['verify-code', root],
|
|
10269
|
+
audit: ['audit', root],
|
|
10270
|
+
handoff: ['handoff', root, '--quiet', '--no-drift-check'],
|
|
10271
|
+
health: ['health', root, '--json']
|
|
10272
|
+
}[op];
|
|
10273
|
+
log(C.dim(` → leerness ${subArgs.join(' ')}`));
|
|
10274
|
+
const t0 = Date.now();
|
|
10275
|
+
const r = runCommandSafe(process.execPath, [__filename, ...subArgs], {
|
|
10276
|
+
cwd: root, root, timeout: 60000, kind: 'agent_repl_slash', label: `repl-${op}`,
|
|
10277
|
+
env: { LEERNESS_NO_BANNER: '1', LEERNESS_NO_PROMPT: '1', LEERNESS_NO_DRIFT_CHECK: '1' }
|
|
10278
|
+
});
|
|
10279
|
+
const dt = Date.now() - t0;
|
|
10280
|
+
if (r.stdout) log(r.stdout.trim().split('\n').slice(0, 30).join('\n'));
|
|
10281
|
+
if (r.status === 0) log(C.green(` ✓ :${op} 완료 (${dt}ms)`));
|
|
10282
|
+
else log(C.yel(` ⚠ :${op} 실패 (exit ${r.status}, ${dt}ms)`));
|
|
10283
|
+
return false;
|
|
10284
|
+
}
|
|
10152
10285
|
log(C.yel(` 알 수 없는 명령: :${op} (:help 참고)`));
|
|
10153
10286
|
return false;
|
|
10154
10287
|
};
|
|
@@ -10208,16 +10341,17 @@ async function agentCmd(root, taskArg) {
|
|
|
10208
10341
|
return;
|
|
10209
10342
|
}
|
|
10210
10343
|
// non-TTY: 사용법만 출력
|
|
10211
|
-
log('# leerness agent (1.9.146/148/149) — Hermes/OpenClaw 스타일 CLI 에이전트');
|
|
10344
|
+
log('# leerness agent (1.9.146/148/149/150) — Hermes/OpenClaw 스타일 CLI 에이전트 + Sandbox');
|
|
10212
10345
|
log('');
|
|
10213
10346
|
log('사용법:');
|
|
10214
|
-
log(' leerness agent #
|
|
10347
|
+
log(' leerness agent # 1.9.149 REPL 모드 (모델 선택 + 채팅)');
|
|
10215
10348
|
log(' leerness agent "<task>" # 1회 위임 (actor 역할 기본)');
|
|
10216
10349
|
log(' leerness agent "<task>" --role planner # 계획만 (1.9.148)');
|
|
10217
10350
|
log(' leerness agent "<task>" --role reviewer # 비판적 검토 (1.9.148)');
|
|
10218
10351
|
log(' leerness agent --interactive --model qwen2.5-coder # 명시적 REPL + model 선택');
|
|
10219
10352
|
log('');
|
|
10220
10353
|
log('REPL 메타 명령: :help / :model / :role / :provider / :history / :save / :quit');
|
|
10354
|
+
log('REPL Slash 명령 (1.9.150): :verify / :audit / :handoff / :health (sandboxed runCommandSafe)');
|
|
10221
10355
|
log('');
|
|
10222
10356
|
log('현재 활성 provider: ' + (_activeCliAgents().join(', ') || '(없음) — .env에서 LEERNESS_ENABLE_* 활성화'));
|
|
10223
10357
|
log('권한 모드: ' + (_readPermissions(root).mode || 'basic'));
|
|
@@ -10575,7 +10709,8 @@ async function deployAutoCmd(root, service) {
|
|
|
10575
10709
|
log(`service: ${service} · command: ${meta.deployCommand}`);
|
|
10576
10710
|
if (has('--dry-run')) { log('(dry-run) 실제 실행 스킵'); return; }
|
|
10577
10711
|
const t0 = Date.now();
|
|
10578
|
-
|
|
10712
|
+
// 1.9.150: runCommandSafe — deploy 명령 sandbox (env scrub + permissions 검증 + observability)
|
|
10713
|
+
const r = runCommandSafe(meta.deployCommand, [], { cwd: root, root, timeout: 10 * 60 * 1000, allowShell: true, stdio: 'inherit', kind: 'deploy_auto', label: `deploy-${service}` });
|
|
10579
10714
|
const dt = Date.now() - t0;
|
|
10580
10715
|
if (r.status === 0) {
|
|
10581
10716
|
ok(`deploy 성공: ${service} (${dt}ms)`);
|