@asc-agent/runtime 0.7.1 → 0.8.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 +1 -1
- package/dist/adapters/claude-code/guard.js +308 -215
- package/dist/adapters/claude-code/install.d.ts +18 -0
- package/dist/adapters/claude-code/install.js +66 -0
- package/dist/adapters/claude-code/skill.js +114 -18
- package/dist/adapters/gitlab/client.d.ts +10 -0
- package/dist/adapters/gitlab/client.js +17 -1
- package/dist/adapters/gitlab/scm.d.ts +31 -0
- package/dist/adapters/gitlab/scm.js +341 -6
- package/dist/cli/asc.js +870 -72
- package/dist/core/attach/bootstrap.js +4 -4
- package/dist/core/attach/setup.js +8 -8
- package/dist/core/distribution/release.d.ts +3 -3
- package/dist/core/distribution/release.js +1 -1
- package/dist/core/distribution/runtime-install.js +1 -1
- package/dist/core/execution/executor.d.ts +44 -0
- package/dist/core/execution/executor.js +108 -14
- package/dist/core/execution/grant.d.ts +8 -0
- package/dist/core/execution/grant.js +2 -0
- package/dist/core/execution/remote-review.d.ts +117 -0
- package/dist/core/execution/remote-review.js +176 -0
- package/dist/core/model/entities.d.ts +69 -0
- package/dist/core/model/entities.js +46 -0
- package/dist/core/model/transitions.d.ts +1 -1
- package/dist/core/policy/execution-mode.d.ts +136 -0
- package/dist/core/policy/execution-mode.js +127 -0
- package/dist/core/policy/remote-freeze.d.ts +2 -2
- package/dist/ports/scm.d.ts +28 -0
- package/dist/schemas/profile.d.ts +6 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ proceed-by-default, escalation, audit, the external-write guard, host integratio
|
|
|
7
7
|
here.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install -g @asc-agent/runtime@0.
|
|
10
|
+
npm install -g @asc-agent/runtime@0.8.0
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
npm owns the executable link (on Windows, npm's own `asc.cmd`). This package never edits
|
|
@@ -156,6 +156,17 @@ export function segmentsOf(command) {
|
|
|
156
156
|
flush();
|
|
157
157
|
return segments;
|
|
158
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* 이 조각이 ASC control-plane 명령인가 (E-02).
|
|
161
|
+
*
|
|
162
|
+
* **Guard 는 ASC 자신의 명령을 절대 막지 않는다.** 막는 쪽과 나가는 쪽이 동시에 닫히면
|
|
163
|
+
* 사람이 갇힌다 — 0.7.1 실측에서 raw write 는 Guard 가, `asc grant issue` 는 Host 가 막아
|
|
164
|
+
* 나갈 길이 없었다. Guard 가 지는 몫은 이 한 줄로 끝난다: 우리 명령은 통과시킨다.
|
|
165
|
+
* 실행할 권한이 있는지는 그 다음에 Core 가 판정한다.
|
|
166
|
+
*/
|
|
167
|
+
function isControlPlane(bare) {
|
|
168
|
+
return /^\s*(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:[\w./~-]*\/)?asc(?:\.[cm]?js)?(?:\s|$)/.test(bare);
|
|
169
|
+
}
|
|
159
170
|
/** 이 조각이 다른 명령을 문자열로 받아 실행하는 자리인가. */
|
|
160
171
|
function runsGivenText(bare) {
|
|
161
172
|
return /(^|\s)(?:sh|bash|zsh|dash|ksh)\s+(?:-[a-zA-Z]*\s+)*-c(\s|$)/.test(bare) || /(^|\s)eval(\s|$)/.test(bare);
|
|
@@ -169,6 +180,9 @@ function runsGivenText(bare) {
|
|
|
169
180
|
*/
|
|
170
181
|
export function forbiddenIn(command, patterns) {
|
|
171
182
|
for (const segment of segmentsOf(command)) {
|
|
183
|
+
// ASC control-plane 은 판정 대상이 아니다 (E-02). 여기가 그 불변식이 사는 한 자리다.
|
|
184
|
+
if (isControlPlane(segment.bare))
|
|
185
|
+
continue;
|
|
172
186
|
for (const { pattern, label } of patterns) {
|
|
173
187
|
if (pattern.test(segment.bare))
|
|
174
188
|
return label;
|
|
@@ -229,224 +243,303 @@ export function hookScript() {
|
|
|
229
243
|
// **판정 로직을 두 번 쓰지 않는다.** 위 함수들의 소스를 그대로 실어 나른다 — 손으로
|
|
230
244
|
// 옮겨 적으면 언젠가 hook 과 단위 검사가 서로 다른 것을 막는다. 그래서 저 함수들은
|
|
231
245
|
// 바깥 식별자를 참조하지 않는다.
|
|
232
|
-
const logic = [segmentsOf, runsGivenText, forbiddenIn]
|
|
246
|
+
const logic = [segmentsOf, isControlPlane, runsGivenText, forbiddenIn]
|
|
233
247
|
.map((fn) => fn.toString())
|
|
234
248
|
.join('\n\n')
|
|
235
249
|
.replace(/`/g, '\\`')
|
|
236
250
|
.replace(/\$\{/g, '\\${');
|
|
237
|
-
return `#!/usr/bin/env node
|
|
238
|
-
// ASC external-write guard (PreToolUse) — 설치·갱신은 \`asc host claude install\` 로만.
|
|
239
|
-
// 관리 대상(ASC RuntimeBinding에 등록된) Claude 세션의 외부 write를 실행 직전에 막는다.
|
|
240
|
-
// ASC와 무관한 프로젝트·세션은 항상 통과한다.
|
|
241
|
-
//
|
|
242
|
-
// 이 파일에는 책임이 둘 있고 섞이면 안 된다:
|
|
243
|
-
// safety — 금지 명령 차단. 실패하면 막아야 할 것이 나간다
|
|
244
|
-
// telemetry — 활동 관찰. 실패하면 화면 한 줄이 빈다
|
|
245
|
-
// telemetry는 try/catch 안에서만 돌고 어떤 exit 경로에도 관여하지 않는다.
|
|
246
|
-
import { readFileSync, readdirSync, existsSync, writeFileSync, renameSync } from 'node:fs'
|
|
247
|
-
import { homedir } from 'node:os'
|
|
248
|
-
import { dirname, join, resolve } from 'node:path'
|
|
249
|
-
|
|
250
|
-
const FORBIDDEN = [
|
|
251
|
-
${patterns}
|
|
252
|
-
]
|
|
253
|
-
|
|
254
|
-
const OFFLINE_ONLY = [
|
|
255
|
-
${offlinePatterns}
|
|
256
|
-
]
|
|
257
|
-
|
|
258
|
-
${logic}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
function
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
251
|
+
return `#!/usr/bin/env node
|
|
252
|
+
// ASC external-write guard (PreToolUse) — 설치·갱신은 \`asc host claude install\` 로만.
|
|
253
|
+
// 관리 대상(ASC RuntimeBinding에 등록된) Claude 세션의 외부 write를 실행 직전에 막는다.
|
|
254
|
+
// ASC와 무관한 프로젝트·세션은 항상 통과한다.
|
|
255
|
+
//
|
|
256
|
+
// 이 파일에는 책임이 둘 있고 섞이면 안 된다:
|
|
257
|
+
// safety — 금지 명령 차단. 실패하면 막아야 할 것이 나간다
|
|
258
|
+
// telemetry — 활동 관찰. 실패하면 화면 한 줄이 빈다
|
|
259
|
+
// telemetry는 try/catch 안에서만 돌고 어떤 exit 경로에도 관여하지 않는다.
|
|
260
|
+
import { readFileSync, readdirSync, existsSync, writeFileSync, renameSync } from 'node:fs'
|
|
261
|
+
import { homedir } from 'node:os'
|
|
262
|
+
import { dirname, join, resolve } from 'node:path'
|
|
263
|
+
|
|
264
|
+
const FORBIDDEN = [
|
|
265
|
+
${patterns}
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
const OFFLINE_ONLY = [
|
|
269
|
+
${offlinePatterns}
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
${logic}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 이 workspace 의 실행 축 (0.8.0 Axis C · 보정 P0-1).
|
|
276
|
+
*
|
|
277
|
+
* **세 자리를 가른다.** 두 개로 뭉치면 어느 쪽이든 한 번은 틀린다:
|
|
278
|
+
*
|
|
279
|
+
* 파일 없음 ADVISE 아무도 고르지 않았다. 검사되지 않은 강제를 켜지 않는다
|
|
280
|
+
* 파일 있고 AUTO ENFORCE 사람이 고르고 readiness 를 통과한 상태다
|
|
281
|
+
* 파일 있고 MANUAL ADVISE 사람이 고른 상태다
|
|
282
|
+
* 파일 있는데 못 읽음 ENFORCE AUTO 였을 수도 있다 — 모르는 것을 여는 쪽으로 기울지 않는다
|
|
283
|
+
*
|
|
284
|
+
* 마지막 자리가 이 함수가 다시 쓰인 이유다. 예전에는 읽기 실패를 MANUAL 로 답했고,
|
|
285
|
+
* 그러면 저장돼 있던 AUTO 가 파일 손상·권한·I/O 하나로 조용히 풀린다 (fail-open).
|
|
286
|
+
* 여기서 강제한다고 해서 AUTO 라고 말하지는 않는다 — 화면에는 "읽지 못했다" 로 나간다.
|
|
287
|
+
*/
|
|
288
|
+
function executionState(ascRoot) {
|
|
289
|
+
const file = join(ascRoot, 'adapters', 'policy', 'execution-mode.json')
|
|
290
|
+
let raw
|
|
291
|
+
try {
|
|
292
|
+
raw = readFileSync(file, 'utf8')
|
|
293
|
+
} catch (error) {
|
|
294
|
+
// 없는 것과 못 읽는 것은 다르다. ENOENT 만 "고르지 않았다" 다.
|
|
295
|
+
if (error && error.code === 'ENOENT') return { enforcement: 'ADVISE', mode: 'MANUAL', chosen: false }
|
|
296
|
+
return { enforcement: 'ENFORCE', degraded: 'MODE_STATE_UNREADABLE' }
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const mode = JSON.parse(JSON.parse(raw).value).mode
|
|
300
|
+
if (mode !== 'AUTO' && mode !== 'MANUAL') return { enforcement: 'ENFORCE', degraded: 'MODE_STATE_INVALID' }
|
|
301
|
+
return { enforcement: mode === 'AUTO' ? 'ENFORCE' : 'ADVISE', mode, chosen: true }
|
|
302
|
+
} catch {
|
|
303
|
+
return { enforcement: 'ENFORCE', degraded: 'MODE_STATE_INVALID' }
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 원격이 얼어 있는가. 얼어 있으면 완전 오프라인인지까지 본다.
|
|
309
|
+
* 읽지 못하면 얼지 않은 것으로 본다 — guard 오작동이 곧 작업 중단이 되면 안 된다.
|
|
310
|
+
*/
|
|
311
|
+
function freezePolicy(ascRoot) {
|
|
312
|
+
try {
|
|
313
|
+
const { value } = JSON.parse(readFileSync(join(ascRoot, 'adapters', 'policy', 'freeze-policy.json'), 'utf8'))
|
|
314
|
+
return JSON.parse(value)
|
|
315
|
+
} catch {
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** 경로 비교용 정규화. index가 쓰는 것과 같은 규칙이어야 한다. */
|
|
321
|
+
function normalizePath(path) {
|
|
322
|
+
const slashed = resolve(path).replace(/\\\\/g, '/').replace(/\\/+$/, '')
|
|
323
|
+
return /^[a-zA-Z]:/.test(slashed) ? slashed[0].toUpperCase() + slashed.slice(1) : slashed
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* user-owned runtime의 역색인에서 이 경로의 workspace를 찾는다.
|
|
328
|
+
*
|
|
329
|
+
* hook은 매 Bash 호출마다 도는 무의존 단일 파일이다 — 그래서 **읽기 한 번, 파싱 한 번**이
|
|
330
|
+
* 상한이다. 조회는 문자열 비교뿐이고 파일시스템을 더 뒤지지 않는다.
|
|
331
|
+
*
|
|
332
|
+
* 반환은 세 갈래다:
|
|
333
|
+
* { root } 이 경로는 등록된 workspace다
|
|
334
|
+
* 'MISSING' 등록은 있는데 runtime을 못 읽는다 — 판정 불능
|
|
335
|
+
* null index 자체가 없거나 이 경로가 등록돼 있지 않다
|
|
336
|
+
*/
|
|
337
|
+
function lookupWorkspace(start) {
|
|
338
|
+
const home = process.env.ASC_HOME || join(homedir(), '.asc')
|
|
339
|
+
let index
|
|
340
|
+
try {
|
|
341
|
+
index = JSON.parse(readFileSync(join(home, 'workspace-index.json'), 'utf8'))
|
|
342
|
+
} catch {
|
|
343
|
+
return null // index가 없으면 user-owned runtime을 쓰지 않는 설치다
|
|
344
|
+
}
|
|
345
|
+
const locators = (index && index.locators) || {}
|
|
346
|
+
let path = normalizePath(start)
|
|
347
|
+
for (;;) {
|
|
348
|
+
const entry = locators[path]
|
|
349
|
+
if (entry) return existsSync(entry.root) ? { root: entry.root } : 'MISSING'
|
|
350
|
+
const parent = path.slice(0, path.lastIndexOf('/'))
|
|
351
|
+
if (!parent || parent === path || /^[a-zA-Z]:$/.test(path)) return null
|
|
352
|
+
path = parent
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** 저장소 안의 .asc — 팀이 채택했거나 아직 이전하지 않은 개인 상태. */
|
|
357
|
+
function findAscRoot(start) {
|
|
358
|
+
let dir = resolve(start)
|
|
359
|
+
const stop = normalizePath(homedir())
|
|
360
|
+
for (;;) {
|
|
361
|
+
// 홈의 ~/.asc 는 user runtime이지 프로젝트 상태가 아니다 — 프로젝트로 읽지 않는다
|
|
362
|
+
if (normalizePath(dir) === stop) return null
|
|
363
|
+
const candidate = join(dir, '.asc')
|
|
364
|
+
if (existsSync(candidate)) return candidate
|
|
365
|
+
const parent = dirname(dir)
|
|
366
|
+
if (parent === dir) return null
|
|
367
|
+
dir = parent
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** 관리 대상 세션을 찾는다. 어느 Logical Session 소속인지까지 알아야 관찰을 남길 수 있다. */
|
|
372
|
+
function findManaged(ascRoot, sessionId) {
|
|
373
|
+
const dir = join(ascRoot, 'adapters', 'claude-code')
|
|
374
|
+
if (!existsSync(dir)) return null
|
|
375
|
+
for (const name of readdirSync(dir)) {
|
|
376
|
+
if (!name.startsWith('runtime-binding') || !name.endsWith('.json')) continue
|
|
377
|
+
try {
|
|
378
|
+
const { value } = JSON.parse(readFileSync(join(dir, name), 'utf8'))
|
|
379
|
+
const binding = JSON.parse(value)
|
|
380
|
+
if (binding.physicalSessionId === sessionId || binding.workerId === sessionId) return binding
|
|
381
|
+
} catch {
|
|
382
|
+
// 깨진 binding은 판별 근거가 못 된다 — 그 항목만 건너뛴다
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return null
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let input
|
|
389
|
+
try {
|
|
390
|
+
input = JSON.parse(readFileSync(0, 'utf8'))
|
|
391
|
+
} catch {
|
|
392
|
+
process.exit(0) // 입력을 못 읽으면 판단하지 않는다 — guard 오작동으로 전부 막는 것이 더 나쁘다
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 파일을 바꾸는 도구는 **일을 시작한다는 신호**다 (F6). 읽기는 여기 없다 — 상태를 보는
|
|
396
|
+
// 세션까지 관리 대상으로 끌어들이면 그것은 자동화가 아니라 방해다.
|
|
397
|
+
const MUTATORS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])
|
|
398
|
+
const isMutation = MUTATORS.has(String(input.tool_name ?? ''))
|
|
399
|
+
if (input.tool_name !== 'Bash' && !isMutation) process.exit(0)
|
|
400
|
+
const command = String(input.tool_input?.command ?? '')
|
|
401
|
+
|
|
402
|
+
const cwd = input.cwd ?? process.cwd()
|
|
403
|
+
const observedSessionId = String(input.session_id ?? '')
|
|
404
|
+
|
|
405
|
+
// 등록된 workspace가 먼저다. 없으면 저장소 안 .asc 로 내려간다 (C-11 §3 우선순위).
|
|
406
|
+
const registered = lookupWorkspace(cwd)
|
|
407
|
+
if (registered === 'MISSING') {
|
|
408
|
+
// 이 경로는 ASC가 맡은 곳인데 runtime을 읽지 못했다. **막지는 않는다** — mode 가 그
|
|
409
|
+
// runtime 안에 있으므로, 읽지 못한 상태에서 차단하면 고르지 않은 enforcement 를 켜는
|
|
410
|
+
// 것이 된다(0.8.0 §B). 대신 그 사실을 말한다: 무엇이 깨졌는지 사람이 알아야 한다.
|
|
411
|
+
if (forbiddenIn(command, FORBIDDEN)) {
|
|
412
|
+
console.error(
|
|
413
|
+
'[ASC] 이 경로는 ASC workspace 로 등록돼 있는데 runtime 을 읽지 못했다 — ' +
|
|
414
|
+
'실행 축(MANUAL/AUTO)을 확인할 수 없다. asc status 로 확인하라.',
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
process.exit(0)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const ascRoot = registered ? registered.root : findAscRoot(cwd)
|
|
421
|
+
// ASC와 무관한 일반 세션이다 — 소유권을 주장하지 않는다
|
|
422
|
+
if (!ascRoot) process.exit(0)
|
|
423
|
+
|
|
424
|
+
const managed = findManaged(ascRoot, observedSessionId)
|
|
425
|
+
const state = executionState(ascRoot)
|
|
426
|
+
|
|
427
|
+
// 관찰은 차단과 섞이지 않는다 — 실패해도 아래 판정에 닿지 않고, 두 mode 모두에서 돈다.
|
|
428
|
+
// 일을 관리하는 것(Agent Management)은 실행을 누가 하느냐(Execution Mode)와 다른 축이다.
|
|
429
|
+
if (managed) {
|
|
430
|
+
try {
|
|
431
|
+
recordActivity(ascRoot, managed, observedSessionId, String(input.tool_name ?? ''))
|
|
432
|
+
} catch {}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// 완전 오프라인 선언이 있을 때만 읽기까지 막는다 — 이것은 Execution Mode 가 아니라
|
|
436
|
+
// 사람이 직접 켠 스위치이므로 두 mode 모두에 선다. 녹이려면 \`asc thaw\`.
|
|
437
|
+
if (!isMutation) {
|
|
438
|
+
const freeze = freezePolicy(ascRoot)
|
|
439
|
+
if (freeze && freeze.frozen && freeze.denyRemoteRead) {
|
|
440
|
+
const offline = forbiddenIn(command, OFFLINE_ONLY)
|
|
441
|
+
if (offline) {
|
|
442
|
+
console.error(
|
|
443
|
+
\`[ASC guard] 완전 오프라인이다\${freeze.reason ? ' (' + freeze.reason + ')' : ''} — '\${offline}' 를 막는다. \` +
|
|
444
|
+
\`로컬 작업은 그대로 된다. 녹이려면 asc thaw.\`,
|
|
445
|
+
)
|
|
446
|
+
process.exit(2)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ── MANUAL — 막지 않는다. 다만 말은 한다 (0.8.0 §F) ─────────────────────────
|
|
452
|
+
//
|
|
453
|
+
// MANUAL 은 ASC OFF 가 아니다: 일 관리·결정권·검수·감사는 그대로 돈다. 달라지는 것은
|
|
454
|
+
// 강제 라우팅 하나다. 그래서 이 자리의 guard 는 감시자가 아니라 조언자다 —
|
|
455
|
+
// 밖으로 나가는 쓰기를 보면 한 줄 남기고 그대로 통과시킨다.
|
|
456
|
+
//
|
|
457
|
+
// **여기서 대상을 해석하지 않는다** (§H). 어느 프로젝트인지·어느 SHA 인지 판정하는 것은
|
|
458
|
+
// Remote Review 의 일이고, 그 검수는 \`asc work publish --review\` 가 읽기만으로 보여 준다.
|
|
459
|
+
if (state.enforcement === 'ADVISE') {
|
|
460
|
+
const outward = forbiddenIn(command, FORBIDDEN)
|
|
461
|
+
if (outward) {
|
|
462
|
+
console.error(
|
|
463
|
+
[
|
|
464
|
+
\`[ASC] MANUAL — '\${outward}' 를 막지 않는다. 이 쓰기는 ASC 가 관리하는 경로 밖으로 나간다.\`,
|
|
465
|
+
' asc work publish --review # 대상·SHA·결합을 읽기만으로 검수한다',
|
|
466
|
+
' asc mode auto # 관리 경로로만 나가게 하려면',
|
|
467
|
+
].join('\\n'),
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
process.exit(0)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ── 실행 축 기록을 읽지 못했다 (P0-1) ────────────────────────────────────────
|
|
474
|
+
//
|
|
475
|
+
// AUTO 라고 말하지 않는다. 그러나 열어 두지도 않는다 — 저장돼 있던 것이 AUTO 였을 수 있고,
|
|
476
|
+
// 그것을 파일 하나로 푸는 것이 fail-open 이다. ASC 명령은 위에서 이미 통과했으므로 복구
|
|
477
|
+
// 경로는 그대로 열려 있다.
|
|
478
|
+
if (state.degraded) {
|
|
479
|
+
const outward = forbiddenIn(command, FORBIDDEN)
|
|
480
|
+
if (outward) {
|
|
481
|
+
console.error(
|
|
482
|
+
[
|
|
483
|
+
\`[ASC guard] \${state.degraded} — 이 workspace 의 실행 축을 읽지 못했다.\`,
|
|
484
|
+
\`'\${outward}' 는 그 상태에서 나가지 않는다. 저장돼 있던 것이 AUTO 였을 수 있다.\`,
|
|
485
|
+
' asc status # 무엇이 깨졌는지 본다',
|
|
486
|
+
' asc mode # 지금 상태를 그대로 보여 준다',
|
|
487
|
+
'ASC 명령은 막히지 않는다 — 복구는 그쪽으로 한다.',
|
|
488
|
+
].join('\\n'),
|
|
489
|
+
)
|
|
490
|
+
process.exit(2)
|
|
491
|
+
}
|
|
492
|
+
process.exit(0)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ── AUTO 에서만 서는 문 ───────────────────────────────────────────────────────
|
|
496
|
+
if (state.mode === 'AUTO') {
|
|
497
|
+
// **일이 시작되는데 논리 세션이 없다** (F6). 사람이 "ASC 적용해" 라고 말해야 했던 자리다.
|
|
498
|
+
// 여기서 막고 다음 한 걸음을 그대로 준다. 세션에 들어간 뒤에는 이 문이 다시 열린다.
|
|
499
|
+
if (isMutation && !managed) {
|
|
500
|
+
const id = observedSessionId || '<this session id>'
|
|
501
|
+
console.error(
|
|
502
|
+
[
|
|
503
|
+
'[ASC] 이 workspace 는 ASC 가 자동 실행(AUTO)으로 관리한다. 파일을 바꾸기 전에 논리 세션 안에 들어가라.',
|
|
504
|
+
' asc work start <WORK-KEY> # 작업 항목이 있으면',
|
|
505
|
+
' asc work start # 이어갈 세션을 고르거나 계약을 제안받는다',
|
|
506
|
+
'읽기·조회는 막지 않는다 — 막는 것은 관리 밖의 변경뿐이다.',
|
|
507
|
+
'자동 실행을 원하지 않으면: asc mode manual',
|
|
508
|
+
].join('\\n'),
|
|
509
|
+
)
|
|
510
|
+
process.exit(2)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// **관리 대상 workspace 인데 이 Run 이 어느 계약에도 들어 있지 않다** (0.7.0 B-1).
|
|
514
|
+
// 읽기는 그대로 통과한다. 막는 것은 밖으로 나가는 쓰기뿐이다.
|
|
515
|
+
if (!managed) {
|
|
516
|
+
const outward = forbiddenIn(command, FORBIDDEN)
|
|
517
|
+
if (outward) {
|
|
518
|
+
const id = observedSessionId || '<this session id>'
|
|
519
|
+
console.error(
|
|
520
|
+
[
|
|
521
|
+
\`[ASC guard] 이 workspace 는 AUTO 로 관리한다. '\${outward}' 는 논리 세션 밖에서 나갈 수 없다.\`,
|
|
522
|
+
' asc work start <WORK-KEY> # 작업 항목이 있으면',
|
|
523
|
+
' asc work start # 이어갈 세션을 고르거나 계약을 제안받는다',
|
|
524
|
+
' asc host claude bind <S-ID> --physical ' + id,
|
|
525
|
+
'자동 실행을 원하지 않으면: asc mode manual',
|
|
526
|
+
].join('\\n'),
|
|
527
|
+
)
|
|
528
|
+
process.exit(2)
|
|
529
|
+
}
|
|
530
|
+
} else if (!isMutation) {
|
|
531
|
+
// 계약 안에서 도는 세션이다. 밖으로 나가는 것은 승인된 실행 경로로만 나간다.
|
|
532
|
+
const forbidden = forbiddenIn(command, FORBIDDEN)
|
|
533
|
+
if (forbidden) {
|
|
534
|
+
console.error(
|
|
535
|
+
\`[ASC guard] '\${forbidden}' 는 AUTO 로 관리되는 세션에서 금지다. \` +
|
|
536
|
+
\`외부 반영은 \\\`asc work publish\\\` 로 나간다 (승인된 Execution Grant).\`,
|
|
537
|
+
)
|
|
538
|
+
process.exit(2) // exit 2 = 도구 실행 차단
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
process.exit(0)
|
|
451
544
|
${observerSnippet()}`;
|
|
452
545
|
}
|
|
@@ -36,6 +36,24 @@ export declare function locate(paths: InstallPaths): {
|
|
|
36
36
|
manifest: string;
|
|
37
37
|
hooks: HookSpec[];
|
|
38
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* ASC control-plane 을 Host 권한 계층에서 통과시키는 규칙 (E-02, 2층).
|
|
41
|
+
*
|
|
42
|
+
* 0.7.1 실측에서 raw 외부 write 는 ASC Guard 가 막고, 그 자리의 안전한 출구인
|
|
43
|
+
* `asc grant issue` 는 Host 의 권한 판정이 막았다. 막는 길과 나가는 길이 동시에 닫히면
|
|
44
|
+
* 사람이 갇힌다. Host 안에서 우리가 손댈 수 있는 자리는 이 한 줄뿐이다 — 우리 명령을
|
|
45
|
+
* 명시적으로 허용 목록에 올린다. **허용하는 것은 ASC CLI 뿐이고**, 실행 권한이 있는지는
|
|
46
|
+
* 그 다음에 Core 가 판정한다 (Guard allows, then Core decides).
|
|
47
|
+
*/
|
|
48
|
+
export declare const CONTROL_PLANE_ALLOW_RULES: readonly string[];
|
|
49
|
+
/** Host 가 ASC control-plane 을 실제로 실행할 수 있는가 (AUTO readiness 의 첫 축). */
|
|
50
|
+
export type ControlPlaneAccess = {
|
|
51
|
+
allowed: boolean;
|
|
52
|
+
/** deny 규칙이 우리 명령을 막고 있는가. 이것이 참이면 AUTO 는 활성화하지 않는다. */
|
|
53
|
+
denied: boolean;
|
|
54
|
+
detail?: string;
|
|
55
|
+
};
|
|
56
|
+
export declare function controlPlaneAccess(paths: InstallPaths): Promise<ControlPlaneAccess>;
|
|
39
57
|
export type InstallOutcome = {
|
|
40
58
|
written: string[];
|
|
41
59
|
skipped: {
|