@asc-agent/runtime 0.1.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/LICENSE +15 -0
- package/README.md +22 -0
- package/dist/adapters/claude-code/binding.d.ts +5 -0
- package/dist/adapters/claude-code/binding.js +14 -0
- package/dist/adapters/claude-code/guard.d.ts +54 -0
- package/dist/adapters/claude-code/guard.js +295 -0
- package/dist/adapters/claude-code/install.d.ts +67 -0
- package/dist/adapters/claude-code/install.js +231 -0
- package/dist/adapters/claude-code/observer.d.ts +38 -0
- package/dist/adapters/claude-code/observer.js +77 -0
- package/dist/adapters/claude-code/probe.d.ts +53 -0
- package/dist/adapters/claude-code/probe.js +129 -0
- package/dist/adapters/claude-code/skill.d.ts +8 -0
- package/dist/adapters/claude-code/skill.js +267 -0
- package/dist/adapters/fixture-work/index.d.ts +34 -0
- package/dist/adapters/fixture-work/index.js +78 -0
- package/dist/adapters/github/adapter.d.ts +32 -0
- package/dist/adapters/github/adapter.js +95 -0
- package/dist/adapters/github/client.d.ts +30 -0
- package/dist/adapters/github/client.js +90 -0
- package/dist/adapters/github/context.d.ts +44 -0
- package/dist/adapters/github/context.js +170 -0
- package/dist/adapters/github/event-source.d.ts +55 -0
- package/dist/adapters/github/event-source.js +183 -0
- package/dist/adapters/github/scm.d.ts +50 -0
- package/dist/adapters/github/scm.js +104 -0
- package/dist/adapters/gitlab/adapter.d.ts +36 -0
- package/dist/adapters/gitlab/adapter.js +117 -0
- package/dist/adapters/gitlab/client.d.ts +30 -0
- package/dist/adapters/gitlab/client.js +47 -0
- package/dist/adapters/gitlab/ports.d.ts +40 -0
- package/dist/adapters/gitlab/ports.js +186 -0
- package/dist/adapters/jam/adapter.d.ts +53 -0
- package/dist/adapters/jam/adapter.js +144 -0
- package/dist/adapters/jam/event-source.d.ts +28 -0
- package/dist/adapters/jam/event-source.js +65 -0
- package/dist/adapters/jam/mcp-client.d.ts +50 -0
- package/dist/adapters/jam/mcp-client.js +218 -0
- package/dist/adapters/jam/ports.d.ts +51 -0
- package/dist/adapters/jam/ports.js +167 -0
- package/dist/adapters/local/identity.d.ts +27 -0
- package/dist/adapters/local/identity.js +40 -0
- package/dist/adapters/local/presentation.d.ts +14 -0
- package/dist/adapters/local/presentation.js +42 -0
- package/dist/adapters/markdown/layout.d.ts +15 -0
- package/dist/adapters/markdown/layout.js +49 -0
- package/dist/adapters/markdown/serialize.d.ts +9 -0
- package/dist/adapters/markdown/serialize.js +120 -0
- package/dist/adapters/markdown/state-store.d.ts +23 -0
- package/dist/adapters/markdown/state-store.js +316 -0
- package/dist/adapters/mattermost/client.d.ts +23 -0
- package/dist/adapters/mattermost/client.js +54 -0
- package/dist/adapters/mattermost/presentation.d.ts +43 -0
- package/dist/adapters/mattermost/presentation.js +88 -0
- package/dist/adapters/memory/mocks.d.ts +87 -0
- package/dist/adapters/memory/mocks.js +163 -0
- package/dist/adapters/memory/runtime-binding.d.ts +23 -0
- package/dist/adapters/memory/runtime-binding.js +118 -0
- package/dist/adapters/memory/state-store.d.ts +17 -0
- package/dist/adapters/memory/state-store.js +125 -0
- package/dist/adapters/text/renderer.d.ts +7 -0
- package/dist/adapters/text/renderer.js +81 -0
- package/dist/adapters/webhook/ingress.d.ts +103 -0
- package/dist/adapters/webhook/ingress.js +150 -0
- package/dist/cli/asc.d.ts +16 -0
- package/dist/cli/asc.js +2914 -0
- package/dist/cli/identity-config.d.ts +7 -0
- package/dist/cli/identity-config.js +31 -0
- package/dist/composition/observe.d.ts +21 -0
- package/dist/composition/observe.js +72 -0
- package/dist/composition/registry.d.ts +33 -0
- package/dist/composition/registry.js +62 -0
- package/dist/composition/runtime.d.ts +63 -0
- package/dist/composition/runtime.js +155 -0
- package/dist/core/approval/service.d.ts +17 -0
- package/dist/core/approval/service.js +129 -0
- package/dist/core/attach/bootstrap.d.ts +102 -0
- package/dist/core/attach/bootstrap.js +178 -0
- package/dist/core/attach/init.d.ts +29 -0
- package/dist/core/attach/init.js +100 -0
- package/dist/core/attach/setup-plan.d.ts +125 -0
- package/dist/core/attach/setup-plan.js +177 -0
- package/dist/core/attach/setup.d.ts +40 -0
- package/dist/core/attach/setup.js +140 -0
- package/dist/core/binding/types.d.ts +93 -0
- package/dist/core/binding/types.js +74 -0
- package/dist/core/distribution/release.d.ts +15 -0
- package/dist/core/distribution/release.js +27 -0
- package/dist/core/distribution/runtime-install.d.ts +50 -0
- package/dist/core/distribution/runtime-install.js +90 -0
- package/dist/core/distribution/runtime-select.d.ts +74 -0
- package/dist/core/distribution/runtime-select.js +149 -0
- package/dist/core/execution/executor.d.ts +51 -0
- package/dist/core/execution/executor.js +106 -0
- package/dist/core/execution/grant.d.ts +42 -0
- package/dist/core/execution/grant.js +78 -0
- package/dist/core/model/entities.d.ts +872 -0
- package/dist/core/model/entities.js +285 -0
- package/dist/core/model/ids.d.ts +41 -0
- package/dist/core/model/ids.js +50 -0
- package/dist/core/model/transitions.d.ts +26 -0
- package/dist/core/model/transitions.js +124 -0
- package/dist/core/monitor/coverage.d.ts +111 -0
- package/dist/core/monitor/coverage.js +129 -0
- package/dist/core/monitor/engine.d.ts +134 -0
- package/dist/core/monitor/engine.js +574 -0
- package/dist/core/monitor/health-alerts.d.ts +35 -0
- package/dist/core/monitor/health-alerts.js +88 -0
- package/dist/core/monitor/investigation.d.ts +82 -0
- package/dist/core/monitor/investigation.js +232 -0
- package/dist/core/monitor/observation.d.ts +79 -0
- package/dist/core/monitor/observation.js +110 -0
- package/dist/core/monitor/relevance.d.ts +47 -0
- package/dist/core/monitor/relevance.js +100 -0
- package/dist/core/monitor/signals.d.ts +59 -0
- package/dist/core/monitor/signals.js +105 -0
- package/dist/core/operator/local-operator.d.ts +60 -0
- package/dist/core/operator/local-operator.js +82 -0
- package/dist/core/operator/preflight.d.ts +60 -0
- package/dist/core/operator/preflight.js +139 -0
- package/dist/core/operator/proceed.d.ts +98 -0
- package/dist/core/operator/proceed.js +167 -0
- package/dist/core/operator/progress.d.ts +124 -0
- package/dist/core/operator/progress.js +135 -0
- package/dist/core/operator/render.d.ts +34 -0
- package/dist/core/operator/render.js +170 -0
- package/dist/core/operator/runtime-binding.d.ts +61 -0
- package/dist/core/operator/runtime-binding.js +22 -0
- package/dist/core/policy/ownership.d.ts +44 -0
- package/dist/core/policy/ownership.js +46 -0
- package/dist/core/policy/policy.d.ts +99 -0
- package/dist/core/policy/policy.js +147 -0
- package/dist/core/policy/remote-freeze.d.ts +101 -0
- package/dist/core/policy/remote-freeze.js +151 -0
- package/dist/core/policy/scope.d.ts +23 -0
- package/dist/core/policy/scope.js +93 -0
- package/dist/core/presentation/digest.d.ts +98 -0
- package/dist/core/presentation/digest.js +160 -0
- package/dist/core/resolver/load.d.ts +121 -0
- package/dist/core/resolver/load.js +246 -0
- package/dist/core/resolver/render.d.ts +4 -0
- package/dist/core/resolver/render.js +62 -0
- package/dist/core/resolver/resolve.d.ts +42 -0
- package/dist/core/resolver/resolve.js +51 -0
- package/dist/core/resolver/version.d.ts +22 -0
- package/dist/core/resolver/version.js +49 -0
- package/dist/core/runtime/audit.d.ts +374 -0
- package/dist/core/runtime/audit.js +454 -0
- package/dist/core/runtime/claims.d.ts +115 -0
- package/dist/core/runtime/claims.js +153 -0
- package/dist/core/runtime/closure.d.ts +73 -0
- package/dist/core/runtime/closure.js +162 -0
- package/dist/core/runtime/controller.d.ts +40 -0
- package/dist/core/runtime/controller.js +121 -0
- package/dist/core/runtime/escalation.d.ts +188 -0
- package/dist/core/runtime/escalation.js +322 -0
- package/dist/core/runtime/execution-state.d.ts +43 -0
- package/dist/core/runtime/execution-state.js +81 -0
- package/dist/core/runtime/front.d.ts +95 -0
- package/dist/core/runtime/front.js +144 -0
- package/dist/core/runtime/orchestrator.d.ts +54 -0
- package/dist/core/runtime/orchestrator.js +98 -0
- package/dist/core/runtime/query.d.ts +184 -0
- package/dist/core/runtime/query.js +213 -0
- package/dist/core/runtime/report.d.ts +33 -0
- package/dist/core/runtime/report.js +108 -0
- package/dist/core/runtime/session.d.ts +156 -0
- package/dist/core/runtime/session.js +281 -0
- package/dist/core/runtime/store-ops.d.ts +22 -0
- package/dist/core/runtime/store-ops.js +26 -0
- package/dist/core/view/build-view.d.ts +36 -0
- package/dist/core/view/build-view.js +131 -0
- package/dist/core/view/decision-view.d.ts +564 -0
- package/dist/core/view/decision-view.js +103 -0
- package/dist/core/workspace/identity.d.ts +82 -0
- package/dist/core/workspace/identity.js +133 -0
- package/dist/core/workspace/index-store.d.ts +140 -0
- package/dist/core/workspace/index-store.js +125 -0
- package/dist/core/workspace/migrate.d.ts +73 -0
- package/dist/core/workspace/migrate.js +123 -0
- package/dist/core/workspace/resolve.d.ts +34 -0
- package/dist/core/workspace/resolve.js +89 -0
- package/dist/ports/adapter.d.ts +35 -0
- package/dist/ports/adapter.js +12 -0
- package/dist/ports/approval.d.ts +98 -0
- package/dist/ports/approval.js +7 -0
- package/dist/ports/change-context.d.ts +22 -0
- package/dist/ports/change-context.js +8 -0
- package/dist/ports/event-source.d.ts +42 -0
- package/dist/ports/event-source.js +6 -0
- package/dist/ports/inventory.d.ts +53 -0
- package/dist/ports/inventory.js +12 -0
- package/dist/ports/presentation.d.ts +62 -0
- package/dist/ports/presentation.js +13 -0
- package/dist/ports/renderer.d.ts +23 -0
- package/dist/ports/renderer.js +7 -0
- package/dist/ports/resource-context.d.ts +51 -0
- package/dist/ports/resource-context.js +9 -0
- package/dist/ports/scm.d.ts +40 -0
- package/dist/ports/scm.js +6 -0
- package/dist/ports/state-store.d.ts +97 -0
- package/dist/ports/state-store.js +12 -0
- package/dist/presets/balanced.json +8 -0
- package/dist/presets/conservative.json +9 -0
- package/dist/presets/lightweight.json +8 -0
- package/dist/profiles/example-team/profile.json +101 -0
- package/dist/profiles/pilot-local/profile.json +56 -0
- package/dist/schemas/profile.d.ts +631 -0
- package/dist/schemas/profile.js +287 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 colosair
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @asc-agent/runtime
|
|
2
|
+
|
|
3
|
+
The ASC (Agent Session Control) runtime: core, CLI, and adapters.
|
|
4
|
+
|
|
5
|
+
This package provides the `asc` command. Everything ASC decides — sessions, checkpoints,
|
|
6
|
+
proceed-by-default, escalation, audit, the external-write guard, host integration — lives
|
|
7
|
+
here.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @asc-agent/runtime@0.1.0
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
npm owns the executable link (on Windows, npm's own `asc.cmd`). This package never edits
|
|
14
|
+
your shell profile or `PATH`.
|
|
15
|
+
|
|
16
|
+
Once installed, `asc` works offline: the local control-plane commands do not require
|
|
17
|
+
network access. Only bootstrap, install and update do.
|
|
18
|
+
|
|
19
|
+
On a machine with nothing installed yet, start from
|
|
20
|
+
[`@asc-agent/bootstrap`](https://www.npmjs.com/package/@asc-agent/bootstrap) instead.
|
|
21
|
+
|
|
22
|
+
See the [repository README](https://github.com/colosair/asc#readme).
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ScopedRuntimeBindings } from '../memory/runtime-binding.ts';
|
|
2
|
+
import type { StateStore } from '../../ports/state-store.ts';
|
|
3
|
+
export declare const CLAUDE_PROVIDER = "claude-code";
|
|
4
|
+
export declare const CLAUDE_SCOPE = "claude-code";
|
|
5
|
+
export declare function claudeBindings(store: StateStore): ScopedRuntimeBindings;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Claude RuntimeBinding — B-14 소유권 계약의 Claude 인스턴스 (C-03 §3).
|
|
2
|
+
//
|
|
3
|
+
// 로직은 ScopedRuntimeBindings 그대로다. 소유권의 원자성 보장을 provider마다 다시 쓰면
|
|
4
|
+
// 언젠가 서로 다른 것을 보장하게 된다 — 여기는 scope와 provider 이름만 고정한다.
|
|
5
|
+
//
|
|
6
|
+
// 이 scope의 파일(.asc/adapters/claude-code/runtime-binding-*.json)은 guard hook의
|
|
7
|
+
// 판별 목록이기도 하다: binding에 등록된 physical session만 ASC 규칙(외부 write 차단)을
|
|
8
|
+
// 받는다. 관리를 주장한 세션만 관리 규칙을 받는 것이다.
|
|
9
|
+
import { ScopedRuntimeBindings } from "../memory/runtime-binding.js";
|
|
10
|
+
export const CLAUDE_PROVIDER = 'claude-code';
|
|
11
|
+
export const CLAUDE_SCOPE = 'claude-code';
|
|
12
|
+
export function claudeBindings(store) {
|
|
13
|
+
return new ScopedRuntimeBindings(store.scope(CLAUDE_SCOPE));
|
|
14
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grant 없이 금지되는 외부 write 명령 패턴 (C-03 §5.3 차단 대상).
|
|
3
|
+
* commit은 local write라 여기 없다 — Session Contract의 몫이다.
|
|
4
|
+
*/
|
|
5
|
+
export declare const FORBIDDEN_COMMAND_PATTERNS: readonly {
|
|
6
|
+
pattern: RegExp;
|
|
7
|
+
label: string;
|
|
8
|
+
}[];
|
|
9
|
+
/**
|
|
10
|
+
* 완전 오프라인일 때만 추가로 막는 것 (지시 §27).
|
|
11
|
+
*
|
|
12
|
+
* 평소에는 읽기를 막지 않는다 — 읽기까지 막으면 조사 자체가 서지 않고, 문제가 되는 것은
|
|
13
|
+
* 대개 쓰기다. 사람이 "완전 오프라인"이라고 명시했을 때만 이 목록이 선다.
|
|
14
|
+
*/
|
|
15
|
+
export declare const OFFLINE_COMMAND_PATTERNS: readonly {
|
|
16
|
+
pattern: RegExp;
|
|
17
|
+
label: string;
|
|
18
|
+
}[];
|
|
19
|
+
/** Claude permission 규칙(2층). settings의 deny 목록 형식. */
|
|
20
|
+
export declare const PERMISSION_DENY_RULES: readonly string[];
|
|
21
|
+
/**
|
|
22
|
+
* 2층 — ASC-managed worker 세션에 주입하는 설정. worker 기동 시 `claude --settings <이 파일>`.
|
|
23
|
+
*
|
|
24
|
+
* user-scope settings.json의 permissions.deny에 넣지 않는 이유: 그건 모든 프로젝트의
|
|
25
|
+
* 모든 세션(사람 포함)에 전역 적용이라, 사용자 본인의 git push까지 영구히 막는다.
|
|
26
|
+
* 2층의 목적은 worker의 권한 계층 차단이지 사용자 통제가 아니다 — 그래서 deny는
|
|
27
|
+
* worker에게 주는 설정 파일에 살고, 파일은 .asc/ 안(untracked)에 있다.
|
|
28
|
+
*/
|
|
29
|
+
export declare function workerSettings(): string;
|
|
30
|
+
export declare function isForbiddenCommand(command: string): {
|
|
31
|
+
forbidden: boolean;
|
|
32
|
+
label?: string;
|
|
33
|
+
};
|
|
34
|
+
/** 1층 — worker에게 주입하는 계약문. 지침이지 enforcement가 아니라고 전제한다. */
|
|
35
|
+
export declare function workerContract(input: {
|
|
36
|
+
logicalSessionId: string;
|
|
37
|
+
goal: string;
|
|
38
|
+
doneCriteria: readonly string[];
|
|
39
|
+
writeBoundary: readonly string[];
|
|
40
|
+
/** 이 일의 주인. 다른 파트에 물었다는 이유로 바뀌지 않는다 (C-04 §1.2). */
|
|
41
|
+
owner?: string;
|
|
42
|
+
/** 결정 영역 → 결정권자. worker가 자기 것이 아닌 결정을 하지 않게 한다. */
|
|
43
|
+
decisionAuthority?: Readonly<Record<string, string>>;
|
|
44
|
+
dependencies?: readonly string[];
|
|
45
|
+
}): string;
|
|
46
|
+
/**
|
|
47
|
+
* 3층 hook 스크립트 본문. user-scope에 설치되며, stdin으로 PreToolUse 입력(JSON)을 받아
|
|
48
|
+
* exit 2로 차단한다. 판별 순서:
|
|
49
|
+
* Bash가 아니면 통과 → cwd에서 .asc를 못 찾으면 통과(ASC 무관 프로젝트)
|
|
50
|
+
* → 관리 대상 세션 목록에 없으면 통과(사람 세션) → 금지 패턴이면 차단
|
|
51
|
+
*
|
|
52
|
+
* 의존성 없는 단일 파일이어야 한다 — hook은 어느 프로젝트에서든 돈다.
|
|
53
|
+
*/
|
|
54
|
+
export declare function hookScript(): string;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// External Write Guard — ASC-managed Claude worker가 Grant 없이 밖에 쓰지 못하게 하는
|
|
2
|
+
// 3층 방어의 정본 (C-03 §5.3).
|
|
3
|
+
//
|
|
4
|
+
// 1층 worker 계약문(prompt) — 지침. 어겨질 수 있다고 전제한다
|
|
5
|
+
// 2층 permission deny 규칙 — Claude 권한 계층의 차단
|
|
6
|
+
// 3층 PreToolUse hook — 실행 직전, 도구가 돌기 전에 막는 마지막 문
|
|
7
|
+
//
|
|
8
|
+
// 한 곳에서만 규칙을 정의한다. 세 층이 각자 목록을 들면 언젠가 서로 다른 것을 막는다.
|
|
9
|
+
//
|
|
10
|
+
// hook은 user-scope에 설치되어 모든 세션의 Bash 앞에 서지만, 차단은 **관리 대상 세션**
|
|
11
|
+
// 에만 적용한다 — 사람 세션의 git push까지 막으면 guard가 아니라 방해다. 관리 대상
|
|
12
|
+
// 여부는 `.asc/adapters/claude-code/`의 RuntimeBinding(physical session id)으로 판별한다.
|
|
13
|
+
// binding이 곧 enforcement 대상 목록이다: ASC가 관리를 주장한 세션만 ASC 규칙을 받는다.
|
|
14
|
+
//
|
|
15
|
+
// hook은 관찰(observer.ts)도 함께 나르지만 두 책임은 섞이지 않는다 — 차단 규칙은 여기,
|
|
16
|
+
// 관찰은 거기, 그리고 관찰 실패는 차단 판정에 닿지 않는다 (B-18).
|
|
17
|
+
import { observerSnippet } from "./observer.js";
|
|
18
|
+
/**
|
|
19
|
+
* Grant 없이 금지되는 외부 write 명령 패턴 (C-03 §5.3 차단 대상).
|
|
20
|
+
* commit은 local write라 여기 없다 — Session Contract의 몫이다.
|
|
21
|
+
*/
|
|
22
|
+
export const FORBIDDEN_COMMAND_PATTERNS = [
|
|
23
|
+
{ pattern: /\bgit\s+(?:[^\s]+\s+)*push\b/, label: 'git push' },
|
|
24
|
+
{ pattern: /\bgh\s+pr\s+(create|edit|ready|close|merge|comment|review)\b/, label: 'gh pr <write>' },
|
|
25
|
+
{ pattern: /\bgh\s+issue\s+(create|edit|comment|close|reopen)\b/, label: 'gh issue <write>' },
|
|
26
|
+
{ pattern: /\bgh\s+release\s+(create|edit|delete)\b/, label: 'gh release <write>' },
|
|
27
|
+
// gh api는 통짜로 막는다. 읽기 호출까지 막히지만, worker가 필요로 하는 조회는
|
|
28
|
+
// asc CLI가 대신한다 — write 성격 판별(-X·--method·-f)을 hook에서 흉내 내다 구멍을
|
|
29
|
+
// 내는 것보다 넓게 막고 좁게 여는 편이 안전하다.
|
|
30
|
+
{ pattern: /\bgh\s+api\b/, label: 'gh api' },
|
|
31
|
+
{ pattern: /\bglab\s+mr\s+(create|merge|close|update|approve)\b/, label: 'glab mr <write>' },
|
|
32
|
+
{ pattern: /\bglab\s+(issue|release)\s+(create|edit|close|update|delete)\b/, label: 'glab <write>' },
|
|
33
|
+
{ pattern: /\bglab\s+api\b/, label: 'glab api' },
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* 완전 오프라인일 때만 추가로 막는 것 (지시 §27).
|
|
37
|
+
*
|
|
38
|
+
* 평소에는 읽기를 막지 않는다 — 읽기까지 막으면 조사 자체가 서지 않고, 문제가 되는 것은
|
|
39
|
+
* 대개 쓰기다. 사람이 "완전 오프라인"이라고 명시했을 때만 이 목록이 선다.
|
|
40
|
+
*/
|
|
41
|
+
export const OFFLINE_COMMAND_PATTERNS = [
|
|
42
|
+
{ pattern: /\bgit\s+(?:[^\s]+\s+)*(fetch|pull|clone|ls-remote)\b/, label: 'git <remote read>' },
|
|
43
|
+
{ pattern: /\bgh\s+(pr|issue|repo|release|run)\s+(list|view|status|checks)\b/, label: 'gh <remote read>' },
|
|
44
|
+
{ pattern: /\bglab\s+(mr|issue|repo|release)\s+(list|view)\b/, label: 'glab <remote read>' },
|
|
45
|
+
];
|
|
46
|
+
/** Claude permission 규칙(2층). settings의 deny 목록 형식. */
|
|
47
|
+
export const PERMISSION_DENY_RULES = [
|
|
48
|
+
'Bash(git push:*)',
|
|
49
|
+
'Bash(gh pr create:*)',
|
|
50
|
+
'Bash(gh pr edit:*)',
|
|
51
|
+
'Bash(gh pr ready:*)',
|
|
52
|
+
'Bash(gh pr close:*)',
|
|
53
|
+
'Bash(gh pr merge:*)',
|
|
54
|
+
'Bash(gh pr comment:*)',
|
|
55
|
+
'Bash(gh pr review:*)',
|
|
56
|
+
'Bash(gh issue create:*)',
|
|
57
|
+
'Bash(gh issue edit:*)',
|
|
58
|
+
'Bash(gh issue comment:*)',
|
|
59
|
+
'Bash(gh issue close:*)',
|
|
60
|
+
'Bash(gh api:*)',
|
|
61
|
+
'Bash(glab mr create:*)',
|
|
62
|
+
'Bash(glab mr merge:*)',
|
|
63
|
+
'Bash(glab api:*)',
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* 2층 — ASC-managed worker 세션에 주입하는 설정. worker 기동 시 `claude --settings <이 파일>`.
|
|
67
|
+
*
|
|
68
|
+
* user-scope settings.json의 permissions.deny에 넣지 않는 이유: 그건 모든 프로젝트의
|
|
69
|
+
* 모든 세션(사람 포함)에 전역 적용이라, 사용자 본인의 git push까지 영구히 막는다.
|
|
70
|
+
* 2층의 목적은 worker의 권한 계층 차단이지 사용자 통제가 아니다 — 그래서 deny는
|
|
71
|
+
* worker에게 주는 설정 파일에 살고, 파일은 .asc/ 안(untracked)에 있다.
|
|
72
|
+
*/
|
|
73
|
+
export function workerSettings() {
|
|
74
|
+
return `${JSON.stringify({
|
|
75
|
+
$comment: 'ASC-managed worker 전용. claude --settings 로 주입한다. asc host claude guard 가 생성·갱신.',
|
|
76
|
+
permissions: { deny: [...PERMISSION_DENY_RULES] },
|
|
77
|
+
}, null, 2)}\n`;
|
|
78
|
+
}
|
|
79
|
+
export function isForbiddenCommand(command) {
|
|
80
|
+
for (const { pattern, label } of FORBIDDEN_COMMAND_PATTERNS) {
|
|
81
|
+
if (pattern.test(command))
|
|
82
|
+
return { forbidden: true, label };
|
|
83
|
+
}
|
|
84
|
+
return { forbidden: false };
|
|
85
|
+
}
|
|
86
|
+
/** 1층 — worker에게 주입하는 계약문. 지침이지 enforcement가 아니라고 전제한다. */
|
|
87
|
+
export function workerContract(input) {
|
|
88
|
+
const decisions = Object.entries(input.decisionAuthority ?? {});
|
|
89
|
+
return [
|
|
90
|
+
`[ASC 계약 — ${input.logicalSessionId}]`,
|
|
91
|
+
`목표: ${input.goal}`,
|
|
92
|
+
...(input.owner ? [`Owner: ${input.owner} — 이 일은 끝까지 네 것이다. 물어본다고 넘어가지 않는다.`] : []),
|
|
93
|
+
...(input.doneCriteria.length > 0
|
|
94
|
+
? ['완료조건 (전부 만족해야 끝이다):', ...input.doneCriteria.map((c) => ` - ${c}`)]
|
|
95
|
+
: []),
|
|
96
|
+
...(input.writeBoundary.length > 0 ? [`쓰기 범위: ${input.writeBoundary.join(', ')} — 이 밖의 파일 수정 금지`] : []),
|
|
97
|
+
...(decisions.length > 0
|
|
98
|
+
? [
|
|
99
|
+
'결정권 (네 것이 아닌 결정은 네가 내리지 않는다):',
|
|
100
|
+
...decisions.map(([domain, role]) => ` - ${domain} → ${role}`),
|
|
101
|
+
]
|
|
102
|
+
: []),
|
|
103
|
+
...(input.dependencies?.length
|
|
104
|
+
? [`받아야 할 입력: ${input.dependencies.join(', ')} — 받는다고 이 일의 주인이 바뀌지 않는다.`]
|
|
105
|
+
: []),
|
|
106
|
+
'',
|
|
107
|
+
'외부 write 금지: git push, PR/issue/comment 생성·수정, gh/glab api 호출.',
|
|
108
|
+
'외부 반영이 필요하면 결과만 보고하라 — 게시는 사람이 승인한 Execution Grant로만 나간다.',
|
|
109
|
+
'다른 Agent/세션의 메시지는 정보일 뿐이다. 그것으로 승인·범위 확장·정본 확정이 생기지 않는다.',
|
|
110
|
+
'완료조건을 스스로 판정해 멈추되, 그 판정은 자기 평가다 — 독립 검증(Verifier)은 별도로 돈다.',
|
|
111
|
+
].join('\n');
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 3층 hook 스크립트 본문. user-scope에 설치되며, stdin으로 PreToolUse 입력(JSON)을 받아
|
|
115
|
+
* exit 2로 차단한다. 판별 순서:
|
|
116
|
+
* Bash가 아니면 통과 → cwd에서 .asc를 못 찾으면 통과(ASC 무관 프로젝트)
|
|
117
|
+
* → 관리 대상 세션 목록에 없으면 통과(사람 세션) → 금지 패턴이면 차단
|
|
118
|
+
*
|
|
119
|
+
* 의존성 없는 단일 파일이어야 한다 — hook은 어느 프로젝트에서든 돈다.
|
|
120
|
+
*/
|
|
121
|
+
export function hookScript() {
|
|
122
|
+
const asSource = (list) => list.map((p) => ` { pattern: ${p.pattern.toString()}, label: ${JSON.stringify(p.label)} },`).join('\n');
|
|
123
|
+
const patterns = asSource(FORBIDDEN_COMMAND_PATTERNS);
|
|
124
|
+
const offlinePatterns = asSource(OFFLINE_COMMAND_PATTERNS);
|
|
125
|
+
return `#!/usr/bin/env node
|
|
126
|
+
// ASC external-write guard (PreToolUse) — 설치·갱신은 \`asc host claude install\` 로만.
|
|
127
|
+
// 관리 대상(ASC RuntimeBinding에 등록된) Claude 세션의 외부 write를 실행 직전에 막는다.
|
|
128
|
+
// ASC와 무관한 프로젝트·세션은 항상 통과한다.
|
|
129
|
+
//
|
|
130
|
+
// 이 파일에는 책임이 둘 있고 섞이면 안 된다:
|
|
131
|
+
// safety — 금지 명령 차단. 실패하면 막아야 할 것이 나간다
|
|
132
|
+
// telemetry — 활동 관찰. 실패하면 화면 한 줄이 빈다
|
|
133
|
+
// telemetry는 try/catch 안에서만 돌고 어떤 exit 경로에도 관여하지 않는다.
|
|
134
|
+
import { readFileSync, readdirSync, existsSync, writeFileSync, renameSync } from 'node:fs'
|
|
135
|
+
import { homedir } from 'node:os'
|
|
136
|
+
import { dirname, join, resolve } from 'node:path'
|
|
137
|
+
|
|
138
|
+
const FORBIDDEN = [
|
|
139
|
+
${patterns}
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
const OFFLINE_ONLY = [
|
|
143
|
+
${offlinePatterns}
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 원격이 얼어 있는가. 얼어 있으면 완전 오프라인인지까지 본다.
|
|
148
|
+
* 읽지 못하면 얼지 않은 것으로 본다 — guard 오작동이 곧 작업 중단이 되면 안 된다.
|
|
149
|
+
*/
|
|
150
|
+
function freezePolicy(ascRoot) {
|
|
151
|
+
try {
|
|
152
|
+
const { value } = JSON.parse(readFileSync(join(ascRoot, 'adapters', 'policy', 'freeze-policy.json'), 'utf8'))
|
|
153
|
+
return JSON.parse(value)
|
|
154
|
+
} catch {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 경로 비교용 정규화. index가 쓰는 것과 같은 규칙이어야 한다. */
|
|
160
|
+
function normalizePath(path) {
|
|
161
|
+
const slashed = resolve(path).replace(/\\\\/g, '/').replace(/\\/+$/, '')
|
|
162
|
+
return /^[a-zA-Z]:/.test(slashed) ? slashed[0].toUpperCase() + slashed.slice(1) : slashed
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* user-owned runtime의 역색인에서 이 경로의 workspace를 찾는다.
|
|
167
|
+
*
|
|
168
|
+
* hook은 매 Bash 호출마다 도는 무의존 단일 파일이다 — 그래서 **읽기 한 번, 파싱 한 번**이
|
|
169
|
+
* 상한이다. 조회는 문자열 비교뿐이고 파일시스템을 더 뒤지지 않는다.
|
|
170
|
+
*
|
|
171
|
+
* 반환은 세 갈래다:
|
|
172
|
+
* { root } 이 경로는 등록된 workspace다
|
|
173
|
+
* 'MISSING' 등록은 있는데 runtime을 못 읽는다 — 판정 불능
|
|
174
|
+
* null index 자체가 없거나 이 경로가 등록돼 있지 않다
|
|
175
|
+
*/
|
|
176
|
+
function lookupWorkspace(start) {
|
|
177
|
+
const home = process.env.ASC_HOME || join(homedir(), '.asc')
|
|
178
|
+
let index
|
|
179
|
+
try {
|
|
180
|
+
index = JSON.parse(readFileSync(join(home, 'workspace-index.json'), 'utf8'))
|
|
181
|
+
} catch {
|
|
182
|
+
return null // index가 없으면 user-owned runtime을 쓰지 않는 설치다
|
|
183
|
+
}
|
|
184
|
+
const locators = (index && index.locators) || {}
|
|
185
|
+
let path = normalizePath(start)
|
|
186
|
+
for (;;) {
|
|
187
|
+
const entry = locators[path]
|
|
188
|
+
if (entry) return existsSync(entry.root) ? { root: entry.root } : 'MISSING'
|
|
189
|
+
const parent = path.slice(0, path.lastIndexOf('/'))
|
|
190
|
+
if (!parent || parent === path || /^[a-zA-Z]:$/.test(path)) return null
|
|
191
|
+
path = parent
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** 저장소 안의 .asc — 팀이 채택했거나 아직 이전하지 않은 개인 상태. */
|
|
196
|
+
function findAscRoot(start) {
|
|
197
|
+
let dir = resolve(start)
|
|
198
|
+
const stop = normalizePath(homedir())
|
|
199
|
+
for (;;) {
|
|
200
|
+
// 홈의 ~/.asc 는 user runtime이지 프로젝트 상태가 아니다 — 프로젝트로 읽지 않는다
|
|
201
|
+
if (normalizePath(dir) === stop) return null
|
|
202
|
+
const candidate = join(dir, '.asc')
|
|
203
|
+
if (existsSync(candidate)) return candidate
|
|
204
|
+
const parent = dirname(dir)
|
|
205
|
+
if (parent === dir) return null
|
|
206
|
+
dir = parent
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** 관리 대상 세션을 찾는다. 어느 Logical Session 소속인지까지 알아야 관찰을 남길 수 있다. */
|
|
211
|
+
function findManaged(ascRoot, sessionId) {
|
|
212
|
+
const dir = join(ascRoot, 'adapters', 'claude-code')
|
|
213
|
+
if (!existsSync(dir)) return null
|
|
214
|
+
for (const name of readdirSync(dir)) {
|
|
215
|
+
if (!name.startsWith('runtime-binding') || !name.endsWith('.json')) continue
|
|
216
|
+
try {
|
|
217
|
+
const { value } = JSON.parse(readFileSync(join(dir, name), 'utf8'))
|
|
218
|
+
const binding = JSON.parse(value)
|
|
219
|
+
if (binding.physicalSessionId === sessionId || binding.workerId === sessionId) return binding
|
|
220
|
+
} catch {
|
|
221
|
+
// 깨진 binding은 판별 근거가 못 된다 — 그 항목만 건너뛴다
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let input
|
|
228
|
+
try {
|
|
229
|
+
input = JSON.parse(readFileSync(0, 'utf8'))
|
|
230
|
+
} catch {
|
|
231
|
+
process.exit(0) // 입력을 못 읽으면 판단하지 않는다 — guard 오작동으로 전부 막는 것이 더 나쁘다
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (input.tool_name !== 'Bash') process.exit(0)
|
|
235
|
+
const command = String(input.tool_input?.command ?? '')
|
|
236
|
+
|
|
237
|
+
const cwd = input.cwd ?? process.cwd()
|
|
238
|
+
const observedSessionId = String(input.session_id ?? '')
|
|
239
|
+
|
|
240
|
+
// 등록된 workspace가 먼저다. 없으면 저장소 안 .asc 로 내려간다 (C-11 §3 우선순위).
|
|
241
|
+
const registered = lookupWorkspace(cwd)
|
|
242
|
+
if (registered === 'MISSING') {
|
|
243
|
+
// **조건부 fail-closed** (C-11 §4). 이 경로는 ASC가 맡은 곳인데 runtime을 읽지 못했다.
|
|
244
|
+
// 그대로 통과시키면 관리 대상 세션의 외부 write가 조용히 열린다 — 그게 가장 나쁘다.
|
|
245
|
+
for (const { pattern, label } of FORBIDDEN) {
|
|
246
|
+
if (pattern.test(command)) {
|
|
247
|
+
console.error(
|
|
248
|
+
\`[ASC guard] 이 경로는 ASC workspace로 등록돼 있는데 runtime을 읽지 못했다. \` +
|
|
249
|
+
\`'\${label}' 를 막는다 — asc setup status 로 확인하라.\`,
|
|
250
|
+
)
|
|
251
|
+
process.exit(2)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
process.exit(0)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const ascRoot = registered ? registered.root : findAscRoot(cwd)
|
|
258
|
+
// ASC와 무관한 일반 세션이다 — 소유권을 주장하지 않는다
|
|
259
|
+
if (!ascRoot) process.exit(0)
|
|
260
|
+
|
|
261
|
+
const managed = findManaged(ascRoot, observedSessionId)
|
|
262
|
+
if (!managed) process.exit(0)
|
|
263
|
+
|
|
264
|
+
// 관찰은 여기서 끝난다 — 아래 차단 판정은 이 호출의 성패를 보지 않는다
|
|
265
|
+
try {
|
|
266
|
+
recordActivity(ascRoot, managed, observedSessionId, String(input.tool_name ?? ''))
|
|
267
|
+
} catch {}
|
|
268
|
+
|
|
269
|
+
for (const { pattern, label } of FORBIDDEN) {
|
|
270
|
+
if (pattern.test(command)) {
|
|
271
|
+
console.error(
|
|
272
|
+
\`[ASC guard] '\${label}' 는 ASC-managed 세션에서 금지다. \` +
|
|
273
|
+
\`외부 반영은 승인된 Execution Grant(asc grant run)로만 나간다.\`,
|
|
274
|
+
)
|
|
275
|
+
process.exit(2) // exit 2 = 도구 실행 차단
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 완전 오프라인 선언이 있을 때만 읽기까지 막는다. 로컬 작업은 얼리지 않는다.
|
|
280
|
+
const freeze = freezePolicy(ascRoot)
|
|
281
|
+
if (freeze && freeze.frozen && freeze.denyRemoteRead) {
|
|
282
|
+
for (const { pattern, label } of OFFLINE_ONLY) {
|
|
283
|
+
if (pattern.test(command)) {
|
|
284
|
+
console.error(
|
|
285
|
+
\`[ASC guard] 완전 오프라인이다\${freeze.reason ? ' (' + freeze.reason + ')' : ''} — '\${label}' 를 막는다. \` +
|
|
286
|
+
\`로컬 작업은 그대로 된다. 녹이려면 asc thaw.\`,
|
|
287
|
+
)
|
|
288
|
+
process.exit(2)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
process.exit(0)
|
|
294
|
+
${observerSnippet()}`;
|
|
295
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type InstallPaths = {
|
|
2
|
+
/** 보통 ~/.claude — 테스트가 격리 디렉터리를 넘긴다. */
|
|
3
|
+
claudeHome: string;
|
|
4
|
+
};
|
|
5
|
+
export declare const defaultPaths: () => InstallPaths;
|
|
6
|
+
export type InstallOutcome = {
|
|
7
|
+
written: string[];
|
|
8
|
+
skipped: {
|
|
9
|
+
path: string;
|
|
10
|
+
reason: string;
|
|
11
|
+
}[];
|
|
12
|
+
hookRegistered: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare function install(paths: InstallPaths, now?: () => string, opts?: {
|
|
15
|
+
force?: boolean;
|
|
16
|
+
}): Promise<InstallOutcome>;
|
|
17
|
+
/**
|
|
18
|
+
* 설치본이 지금 source와 같은가 (기존 한계 L-5).
|
|
19
|
+
*
|
|
20
|
+
* 예전에는 manifest digest만 봤다. manifest는 **설치 당시** 무엇을 썼는지를 적은 것이라,
|
|
21
|
+
* 그 뒤 hook·skill source가 전진해도 "설치됨"이라고 답했다. 패키지가 올라가도 사용자의
|
|
22
|
+
* `~/.claude` 는 옛 내용인데 아무도 그것을 모르는 상태가 그렇게 생긴다.
|
|
23
|
+
*
|
|
24
|
+
* 그래서 세 값을 견준다:
|
|
25
|
+
*
|
|
26
|
+
* ```text
|
|
27
|
+
* S = 지금 source가 만들어 낼 내용 (skillBundle() · hookScript())
|
|
28
|
+
* M = manifest에 적힌 설치 당시 내용
|
|
29
|
+
* I = 지금 설치돼 있는 내용
|
|
30
|
+
*
|
|
31
|
+
* I == S → current 같다
|
|
32
|
+
* I == M 그리고 S != M → stale 사용자는 안 고쳤고 source만 전진했다
|
|
33
|
+
* I != M 그리고 I != S → modified 설치 뒤 사람이 고쳤다
|
|
34
|
+
* 파일 없음 → missing
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* **stale과 modified를 섞지 않는다.** 섞으면 업그레이드가 사람의 수정을 지우거나,
|
|
38
|
+
* 반대로 낡은 설치본이 영영 갱신되지 않는다.
|
|
39
|
+
*/
|
|
40
|
+
export type InstallStatus = 'NOT_INSTALLED' | 'INSTALLED_CURRENT' | 'INSTALLED_STALE' | 'INSTALLED_MODIFIED' | 'BROKEN';
|
|
41
|
+
export type FileState = 'current' | 'stale' | 'modified' | 'missing';
|
|
42
|
+
export type InstallReport = {
|
|
43
|
+
status: InstallStatus;
|
|
44
|
+
files: {
|
|
45
|
+
path: string;
|
|
46
|
+
state: FileState;
|
|
47
|
+
}[];
|
|
48
|
+
/** settings.json 의 PreToolUse에 우리 hook이 지금 경로로 등록돼 있는가. */
|
|
49
|
+
hookRegistered: boolean;
|
|
50
|
+
};
|
|
51
|
+
export declare function verifyInstall(paths: InstallPaths): Promise<InstallReport>;
|
|
52
|
+
/**
|
|
53
|
+
* 설치가 지금 source 그대로인가. 기존 호출부(setup 판정·probe)는 참/거짓만 필요하다.
|
|
54
|
+
* **stale도 거짓이다** — 낡은 설치본을 "설치됨"이라 부르면 L-5가 그대로 남는다.
|
|
55
|
+
*/
|
|
56
|
+
export declare function verifyInstalled(paths: InstallPaths): Promise<boolean>;
|
|
57
|
+
/** 사람이 읽는 줄. 상태마다 다음에 할 일이 다르므로 그것까지 말한다. */
|
|
58
|
+
export declare function installReportLines(report: InstallReport): string[];
|
|
59
|
+
export type UninstallOutcome = {
|
|
60
|
+
removed: string[];
|
|
61
|
+
kept: {
|
|
62
|
+
path: string;
|
|
63
|
+
reason: string;
|
|
64
|
+
}[];
|
|
65
|
+
};
|
|
66
|
+
/** manifest로 증명되는 것만 제거한다. 사용자가 고친 파일은 남기고 이유를 말한다. */
|
|
67
|
+
export declare function uninstall(paths: InstallPaths): Promise<UninstallOutcome>;
|