@asc-agent/runtime 0.7.0 → 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.
@@ -0,0 +1,176 @@
1
+ // Remote Review — 밖으로 나가기 전에 **사실을 검수한다** (0.8.0 보정 §D·§E).
2
+ //
3
+ // 승인과 검수는 다른 일이다:
4
+ //
5
+ // Decision Authority 이 행동을 해도 되는가 — 사람의 자리
6
+ // Remote Review 지금 그 행동이 말이 되는가 — 사실의 자리
7
+ //
8
+ // 사람이 "게시해" 라고 말한 것은 결정권을 해결한다. 그 말이 대상 프로젝트가 맞는지,
9
+ // 승인한 SHA 가 아직 그 SHA 인지, 같은 MR 이 이미 있는지까지 확인해 주지는 않는다.
10
+ // 그것을 확인하는 것이 이 파일이고, **그래서 이것은 두 번째 승인 벽이 아니다** — 사람에게
11
+ // 다시 허락을 구하지 않고, 사실이 어긋났을 때만 멈춘다.
12
+ //
13
+ // 판정은 하나다. MANUAL 에서는 같은 판정이 조언으로 보이고, AUTO 에서는 같은 판정이
14
+ // 실행 전 관문이 된다. 두 벌을 만들면 언젠가 사람이 보는 것과 Agent 가 따르는 것이 갈린다.
15
+ //
16
+ // 이 파일은 순수하다. 원격을 읽지 않는다 — 읽은 결과를 받아 판정만 한다.
17
+ /**
18
+ * 사실을 판정한다. **모르는 것을 무조건 차단으로 번역하지 않는다** (§E) — 모르는 것은
19
+ * 사람이 볼 일이고, 성립하지 않는 것만 실행 불가다.
20
+ */
21
+ export function reviewExternalAction(input) {
22
+ const findings = [];
23
+ const facts = input.facts;
24
+ if (input.requireVerification === true && facts.verifiable === false) {
25
+ findings.push({
26
+ code: 'NO_VERIFY_PATH',
27
+ severity: 'BLOCK',
28
+ detail: `${facts.provider} cannot read back '${input.action}' — it will not run unattended`,
29
+ });
30
+ }
31
+ if (facts.capability === false) {
32
+ findings.push({
33
+ code: 'NO_CAPABILITY',
34
+ severity: 'BLOCK',
35
+ detail: `${facts.provider} cannot carry out '${input.action}'`,
36
+ });
37
+ }
38
+ if (input.target.trim() === '') {
39
+ findings.push({ code: 'MISSING_TARGET', severity: 'BLOCK', detail: 'the action has no target' });
40
+ }
41
+ // 결합 밖의 대상 — Agent 가 스스로 범위를 넓히지 않는다 (§K). 사람이 정할 일이다.
42
+ const bound = input.basis?.resource;
43
+ if (bound && facts.resource && normalize(bound) !== normalize(facts.resource)) {
44
+ findings.push({
45
+ code: 'BINDING_MISMATCH',
46
+ severity: 'REVIEW',
47
+ detail: `this run is bound to ${bound}, and the target is ${facts.resource}`,
48
+ });
49
+ }
50
+ // 승인이 못 박은 것과 지금이 다르다 — 승인은 그 SHA 에 대한 것이었다 (§L).
51
+ const head = facts.observed?.['local.head'];
52
+ if (input.basis?.sourceSha && head && head !== input.basis.sourceSha) {
53
+ findings.push({
54
+ code: 'DRIFT',
55
+ severity: 'BLOCK',
56
+ detail: `what was approved is ${short(input.basis.sourceSha)}, and here it is now ${short(head)}`,
57
+ });
58
+ }
59
+ const remote = facts.observed?.['remote.sha'];
60
+ if (input.basis?.remoteBaseline && remote && remote !== input.basis.remoteBaseline) {
61
+ findings.push({
62
+ code: 'DRIFT',
63
+ severity: 'BLOCK',
64
+ detail: `the remote moved since this was approved (${short(input.basis.remoteBaseline)} → ${short(remote)})`,
65
+ });
66
+ }
67
+ for (const reason of facts.ambiguity ?? []) {
68
+ findings.push({ code: 'AMBIGUOUS', severity: 'REVIEW', detail: reason });
69
+ }
70
+ for (const reason of facts.unknown ?? []) {
71
+ findings.push({ code: 'UNKNOWN_FACT', severity: 'REVIEW', detail: `could not read: ${reason}` });
72
+ }
73
+ const verdict = findings.some((finding) => finding.severity === 'BLOCK')
74
+ ? 'NOT_EXECUTABLE'
75
+ : findings.some((finding) => finding.severity === 'REVIEW')
76
+ ? 'REVIEW_REQUIRED'
77
+ : 'READY';
78
+ return { verdict, findings, expected: expectationOf(input) };
79
+ }
80
+ /**
81
+ * 성공했다면 밖에서 무엇이 보여야 하는가.
82
+ *
83
+ * 실행 전에 적어 두는 이유는 하나다: 실행 뒤에 기대치를 정하면 관측한 것이 기대치가 된다.
84
+ */
85
+ export function expectationOf(input) {
86
+ const expected = { action: input.action, target: input.target };
87
+ const sha = input.basis?.sourceSha ?? input.facts.observed?.['local.head'];
88
+ if (sha)
89
+ expected['sha'] = sha;
90
+ if (input.facts.resource)
91
+ expected['resource'] = input.facts.resource;
92
+ // provider 가 "성공했다면 이것이 보여야 한다" 고 적어 둔 것들. Core 는 그 뜻을 풀지
93
+ // 않고 이름만 옮긴다 — 무엇을 확인할지는 그 행위를 아는 쪽이 안다.
94
+ for (const [key, value] of Object.entries(input.facts.observed ?? {})) {
95
+ if (key.startsWith('expect.') && value !== undefined)
96
+ expected[key.slice('expect.'.length)] = value;
97
+ }
98
+ return expected;
99
+ }
100
+ /**
101
+ * 실행 직전의 재확인 — **바뀔 수 있는 것만 본다.**
102
+ *
103
+ * 검수(CHECK)는 계약을 만들 때 이미 끝났다. 그때 사람이 보고 넘어간 모호함·읽지 못한
104
+ * 사실을 실행 직전에 다시 꺼내면 그것은 두 번째 승인 벽이 된다 — 같은 질문에 두 번
105
+ * 답하게 만드는 구조이고, 이 릴리스가 없애려는 바로 그 형태다.
106
+ *
107
+ * 그래서 여기서 묻는 것은 **승인 이후 실제로 움직일 수 있는 것** 셋뿐이다:
108
+ *
109
+ * ```text
110
+ * 이 통로가 아직 이 행위를 할 수 있는가
111
+ * 대상이 아직 승인된 범위 안인가
112
+ * 못 박은 commit·기준선이 아직 그대로인가
113
+ * ```
114
+ *
115
+ * 답은 둘이다: 그대로면 `null`, 아니면 실행하지 않을 이유 하나.
116
+ */
117
+ export function revalidate(input) {
118
+ const { facts, basis } = input;
119
+ if (facts.capability === false) {
120
+ return { code: 'NO_CAPABILITY', detail: `${facts.provider} cannot carry out '${input.action}'` };
121
+ }
122
+ if (basis?.resource && facts.resource && normalize(basis.resource) !== normalize(facts.resource)) {
123
+ return {
124
+ code: 'BINDING_MISMATCH',
125
+ detail: `this run is bound to ${basis.resource}, and the target is ${facts.resource}`,
126
+ };
127
+ }
128
+ const head = facts.observed?.['local.head'];
129
+ if (basis?.sourceSha && head && head !== basis.sourceSha) {
130
+ return {
131
+ code: 'DRIFT',
132
+ detail: `what was approved is ${short(basis.sourceSha)}, and here it is now ${short(head)}`,
133
+ };
134
+ }
135
+ const remote = facts.observed?.['remote.sha'];
136
+ if (basis?.remoteBaseline && remote && remote !== basis.remoteBaseline) {
137
+ return {
138
+ code: 'DRIFT',
139
+ detail: `the remote moved since this was approved (${short(basis.remoteBaseline)} → ${short(remote)})`,
140
+ };
141
+ }
142
+ return null;
143
+ }
144
+ /** 사람이 읽는 한 줄들. 화면 형태는 MANUAL·AUTO 가 각자 정하고 판정은 같은 것을 쓴다. */
145
+ export function reviewLines(outcome) {
146
+ const lines = [`Remote review: ${outcome.verdict}`];
147
+ for (const finding of outcome.findings) {
148
+ lines.push(` [${finding.severity}] ${finding.code} — ${finding.detail}`);
149
+ }
150
+ return lines;
151
+ }
152
+ /** 원격 신원 비교용 정규화. 대소문자·`.git`·앞뒤 슬래시만 걷어낸다. */
153
+ export function normalize(resource) {
154
+ return resource
155
+ .trim()
156
+ .toLowerCase()
157
+ .replace(/\.git$/, '')
158
+ .replace(/^\/+|\/+$/g, '');
159
+ }
160
+ const short = (sha) => (sha.length > 10 ? sha.slice(0, 10) : sha);
161
+ export function verifyAgainst(expected, observed, keys) {
162
+ const mismatches = [];
163
+ for (const key of keys) {
164
+ const want = expected[key];
165
+ const got = observed[key];
166
+ if (want === undefined)
167
+ continue;
168
+ if (got === undefined) {
169
+ mismatches.push(`${key}: expected ${short(want)}, and it could not be read back`);
170
+ continue;
171
+ }
172
+ if (normalize(want) !== normalize(got))
173
+ mismatches.push(`${key}: expected ${short(want)}, read back ${short(got)}`);
174
+ }
175
+ return { ok: mismatches.length === 0, observed: { ...observed }, mismatches };
176
+ }
@@ -583,6 +583,21 @@ export type ApprovalDecision = z.infer<typeof ApprovalDecision>;
583
583
  /** Grant lifecycle (OM §11.5). 성공한 Grant는 재소비 불가 — replay guard의 근간. */
584
584
  export declare const GrantStatus: z.ZodEnum<["READY", "CLAIMED", "EXECUTED", "INVALIDATED", "EXPIRED"]>;
585
585
  export type GrantStatus = z.infer<typeof GrantStatus>;
586
+ /**
587
+ * terminal 로 간 이유 (0.8.0 보정).
588
+ *
589
+ * ```text
590
+ * DRIFT 승인 근거가 움직였다 — 나가지 않았다
591
+ * NOT_EXECUTABLE 지금 성립하지 않는 행위다 — 나가지 않았다
592
+ * REVIEW_REQUIRED 사람이 봐야 하는 사실이 있다 — 나가지 않았다
593
+ * FORBIDDEN 계약이 허용하지 않는 행위다 — 나가지 않았다
594
+ * REJECTED 밖이 거절했다 — 나가지 않은 것이 확인됐다
595
+ * UNCERTAIN 나갔는지 모른다 — 다시 부르지 않는다
596
+ * NOT_VERIFIED 나갔는데 되돌려 읽은 것이 다르다 — 성공이 아니다
597
+ * ```
598
+ */
599
+ export declare const GrantResolution: z.ZodEnum<["DRIFT", "NOT_EXECUTABLE", "REVIEW_REQUIRED", "FORBIDDEN", "REJECTED", "UNCERTAIN", "NOT_VERIFIED"]>;
600
+ export type GrantResolution = z.infer<typeof GrantResolution>;
586
601
  /**
587
602
  * Policy hierarchy의 하위 override가 아니라, Controller가 hierarchy 밖에서 생성하는
588
603
  * one-shot execution contract (OM §5.2). Session 권한은 그대로 두고 별도 Executor에게만
@@ -621,10 +636,40 @@ export declare const ExecutionGrant: z.ZodEffects<z.ZodObject<{
621
636
  baseline: string;
622
637
  }>, "many">>;
623
638
  threadLastEventId: z.ZodOptional<z.ZodString>;
639
+ /**
640
+ * 승인이 못 박은 사실 (0.8.0 §L).
641
+ *
642
+ * 가지 이름은 그대로인데 내용이 달라질 수 있다 — "이 브랜치를 올려" 는 승인 시점의
643
+ * 그 commit 에 대한 것이었다. 실행 직전 재검수가 이 값과 지금을 견주고, 다르면 나가지
644
+ * 않는다. 없는 경우도 있다(모든 행위가 SHA 를 갖지는 않는다) — 없으면 그 항목은 보지
645
+ * 않을 뿐, 없는 것을 맞다고 치지 않는다.
646
+ */
647
+ basis: z.ZodOptional<z.ZodObject<{
648
+ sourceSha: z.ZodOptional<z.ZodString>;
649
+ remoteBaseline: z.ZodOptional<z.ZodString>;
650
+ /** 이 결합이 가리키는 신원. 대상이 여기서 벗어나면 관리 범위 밖이다. */
651
+ resource: z.ZodOptional<z.ZodString>;
652
+ }, "strip", z.ZodTypeAny, {
653
+ sourceSha?: string | undefined;
654
+ remoteBaseline?: string | undefined;
655
+ resource?: string | undefined;
656
+ }, {
657
+ sourceSha?: string | undefined;
658
+ remoteBaseline?: string | undefined;
659
+ resource?: string | undefined;
660
+ }>>;
624
661
  allowedWrites: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
625
662
  claimedBy: z.ZodOptional<z.ZodString>;
626
663
  consumedAt: z.ZodOptional<z.ZodString>;
627
664
  resultRef: z.ZodOptional<z.ZodString>;
665
+ /**
666
+ * 어떻게 끝났는가 (0.8.0 보정 P0-5·P1-1). **새 상태가 아니라 이유다** — 상태는 그대로
667
+ * 다섯이고, 이 필드는 그 중 terminal 로 간 이유를 구조화해 남긴다.
668
+ *
669
+ * 소진된 것과 성공한 것을 가르는 자리이기도 하다: 밖으로 나갔지만 되돌려 읽은 것이
670
+ * 다르면 그 Grant 는 다시 쓸 수 없고(INVALIDATED), 동시에 성공도 아니다.
671
+ */
672
+ resolution: z.ZodOptional<z.ZodEnum<["DRIFT", "NOT_EXECUTABLE", "REVIEW_REQUIRED", "FORBIDDEN", "REJECTED", "UNCERTAIN", "NOT_VERIFIED"]>>;
628
673
  }, "strip", z.ZodTypeAny, {
629
674
  status: "READY" | "CLAIMED" | "EXECUTED" | "INVALIDATED" | "EXPIRED";
630
675
  snapshot: {
@@ -645,8 +690,14 @@ export declare const ExecutionGrant: z.ZodEffects<z.ZodObject<{
645
690
  resultRef?: string | undefined;
646
691
  requestId?: string | undefined;
647
692
  sessionId?: string | undefined;
693
+ basis?: {
694
+ sourceSha?: string | undefined;
695
+ remoteBaseline?: string | undefined;
696
+ resource?: string | undefined;
697
+ } | undefined;
648
698
  claimedBy?: string | undefined;
649
699
  consumedAt?: string | undefined;
700
+ resolution?: "DRIFT" | "NOT_EXECUTABLE" | "REVIEW_REQUIRED" | "FORBIDDEN" | "REJECTED" | "UNCERTAIN" | "NOT_VERIFIED" | undefined;
650
701
  }, {
651
702
  status: "READY" | "CLAIMED" | "EXECUTED" | "INVALIDATED" | "EXPIRED";
652
703
  id: string;
@@ -666,9 +717,15 @@ export declare const ExecutionGrant: z.ZodEffects<z.ZodObject<{
666
717
  requestId?: string | undefined;
667
718
  sessionId?: string | undefined;
668
719
  singleUse?: boolean | undefined;
720
+ basis?: {
721
+ sourceSha?: string | undefined;
722
+ remoteBaseline?: string | undefined;
723
+ resource?: string | undefined;
724
+ } | undefined;
669
725
  allowedWrites?: string[] | undefined;
670
726
  claimedBy?: string | undefined;
671
727
  consumedAt?: string | undefined;
728
+ resolution?: "DRIFT" | "NOT_EXECUTABLE" | "REVIEW_REQUIRED" | "FORBIDDEN" | "REJECTED" | "UNCERTAIN" | "NOT_VERIFIED" | undefined;
672
729
  }>, {
673
730
  status: "READY" | "CLAIMED" | "EXECUTED" | "INVALIDATED" | "EXPIRED";
674
731
  snapshot: {
@@ -689,8 +746,14 @@ export declare const ExecutionGrant: z.ZodEffects<z.ZodObject<{
689
746
  resultRef?: string | undefined;
690
747
  requestId?: string | undefined;
691
748
  sessionId?: string | undefined;
749
+ basis?: {
750
+ sourceSha?: string | undefined;
751
+ remoteBaseline?: string | undefined;
752
+ resource?: string | undefined;
753
+ } | undefined;
692
754
  claimedBy?: string | undefined;
693
755
  consumedAt?: string | undefined;
756
+ resolution?: "DRIFT" | "NOT_EXECUTABLE" | "REVIEW_REQUIRED" | "FORBIDDEN" | "REJECTED" | "UNCERTAIN" | "NOT_VERIFIED" | undefined;
694
757
  }, {
695
758
  status: "READY" | "CLAIMED" | "EXECUTED" | "INVALIDATED" | "EXPIRED";
696
759
  id: string;
@@ -710,9 +773,15 @@ export declare const ExecutionGrant: z.ZodEffects<z.ZodObject<{
710
773
  requestId?: string | undefined;
711
774
  sessionId?: string | undefined;
712
775
  singleUse?: boolean | undefined;
776
+ basis?: {
777
+ sourceSha?: string | undefined;
778
+ remoteBaseline?: string | undefined;
779
+ resource?: string | undefined;
780
+ } | undefined;
713
781
  allowedWrites?: string[] | undefined;
714
782
  claimedBy?: string | undefined;
715
783
  consumedAt?: string | undefined;
784
+ resolution?: "DRIFT" | "NOT_EXECUTABLE" | "REVIEW_REQUIRED" | "FORBIDDEN" | "REJECTED" | "UNCERTAIN" | "NOT_VERIFIED" | undefined;
716
785
  }>;
717
786
  export type ExecutionGrant = z.infer<typeof ExecutionGrant>;
718
787
  /** Phase B 처리 결과 — 부분 실패해도 cursor는 전진하고 실패분만 재시도한다 (OM §10.5). */
@@ -185,6 +185,28 @@ export const ApprovalDecision = z.object({
185
185
  // ── ExecutionGrant ──────────────────────────────────────────────────────────
186
186
  /** Grant lifecycle (OM §11.5). 성공한 Grant는 재소비 불가 — replay guard의 근간. */
187
187
  export const GrantStatus = z.enum(['READY', 'CLAIMED', 'EXECUTED', 'INVALIDATED', 'EXPIRED']);
188
+ /**
189
+ * terminal 로 간 이유 (0.8.0 보정).
190
+ *
191
+ * ```text
192
+ * DRIFT 승인 근거가 움직였다 — 나가지 않았다
193
+ * NOT_EXECUTABLE 지금 성립하지 않는 행위다 — 나가지 않았다
194
+ * REVIEW_REQUIRED 사람이 봐야 하는 사실이 있다 — 나가지 않았다
195
+ * FORBIDDEN 계약이 허용하지 않는 행위다 — 나가지 않았다
196
+ * REJECTED 밖이 거절했다 — 나가지 않은 것이 확인됐다
197
+ * UNCERTAIN 나갔는지 모른다 — 다시 부르지 않는다
198
+ * NOT_VERIFIED 나갔는데 되돌려 읽은 것이 다르다 — 성공이 아니다
199
+ * ```
200
+ */
201
+ export const GrantResolution = z.enum([
202
+ 'DRIFT',
203
+ 'NOT_EXECUTABLE',
204
+ 'REVIEW_REQUIRED',
205
+ 'FORBIDDEN',
206
+ 'REJECTED',
207
+ 'UNCERTAIN',
208
+ 'NOT_VERIFIED',
209
+ ]);
188
210
  /**
189
211
  * Policy hierarchy의 하위 override가 아니라, Controller가 hierarchy 밖에서 생성하는
190
212
  * one-shot execution contract (OM §5.2). Session 권한은 그대로 두고 별도 Executor에게만
@@ -214,10 +236,34 @@ export const ExecutionGrant = z.object({
214
236
  /** 게시 직전 Drift Guard가 대조할 기준 (OM §11.9). */
215
237
  snapshot: z.array(CanonicalSnapshot).default([]),
216
238
  threadLastEventId: z.string().optional(),
239
+ /**
240
+ * 승인이 못 박은 사실 (0.8.0 §L).
241
+ *
242
+ * 가지 이름은 그대로인데 내용이 달라질 수 있다 — "이 브랜치를 올려" 는 승인 시점의
243
+ * 그 commit 에 대한 것이었다. 실행 직전 재검수가 이 값과 지금을 견주고, 다르면 나가지
244
+ * 않는다. 없는 경우도 있다(모든 행위가 SHA 를 갖지는 않는다) — 없으면 그 항목은 보지
245
+ * 않을 뿐, 없는 것을 맞다고 치지 않는다.
246
+ */
247
+ basis: z
248
+ .object({
249
+ sourceSha: z.string().optional(),
250
+ remoteBaseline: z.string().optional(),
251
+ /** 이 결합이 가리키는 신원. 대상이 여기서 벗어나면 관리 범위 밖이다. */
252
+ resource: z.string().optional(),
253
+ })
254
+ .optional(),
217
255
  allowedWrites: z.array(z.string()).default([]), // 명시된 것 외 모든 write 금지
218
256
  claimedBy: z.string().optional(),
219
257
  consumedAt: Timestamp.optional(),
220
258
  resultRef: z.string().optional(),
259
+ /**
260
+ * 어떻게 끝났는가 (0.8.0 보정 P0-5·P1-1). **새 상태가 아니라 이유다** — 상태는 그대로
261
+ * 다섯이고, 이 필드는 그 중 terminal 로 간 이유를 구조화해 남긴다.
262
+ *
263
+ * 소진된 것과 성공한 것을 가르는 자리이기도 하다: 밖으로 나갔지만 되돌려 읽은 것이
264
+ * 다르면 그 Grant 는 다시 쓸 수 없고(INVALIDATED), 동시에 성공도 아니다.
265
+ */
266
+ resolution: GrantResolution.optional(),
221
267
  })
222
268
  .refine((grant) => Boolean(grant.requestId) !== Boolean(grant.sessionId), {
223
269
  message: 'a grant stands on exactly one basis — an approved request or a session',
@@ -19,6 +19,6 @@ export declare function transitionSession(session: Session, to: Session['status'
19
19
  export declare const REQUEST_TRANSITIONS: readonly TransitionRule<ApprovalRequest['status']>[];
20
20
  export declare function transitionRequest(request: ApprovalRequest, to: ApprovalRequest['status'], actor: ActorRole, patch?: Partial<Pick<ApprovalRequest, 'decision' | 'resultRef'>>): ApprovalRequest;
21
21
  export declare const GRANT_TRANSITIONS: readonly TransitionRule<ExecutionGrant['status']>[];
22
- export declare function transitionGrant(grant: ExecutionGrant, to: ExecutionGrant['status'], actor: ActorRole, patch?: Partial<Pick<ExecutionGrant, 'claimedBy' | 'consumedAt' | 'resultRef'>>): ExecutionGrant;
22
+ export declare function transitionGrant(grant: ExecutionGrant, to: ExecutionGrant['status'], actor: ActorRole, patch?: Partial<Pick<ExecutionGrant, 'claimedBy' | 'consumedAt' | 'resultRef' | 'resolution'>>): ExecutionGrant;
23
23
  export declare const EVENT_TRANSITIONS: readonly TransitionRule<MonitorEvent['processing']>[];
24
24
  export declare function transitionEvent(event: MonitorEvent, to: MonitorEvent['processing'], actor: ActorRole, patch?: Partial<Pick<MonitorEvent, 'requestId'>>): MonitorEvent;
@@ -0,0 +1,136 @@
1
+ import { z } from 'zod';
2
+ import type { ScopedStore } from '../../ports/state-store.ts';
3
+ /**
4
+ * MANUAL ASC 가 작업·판단·기록을 관리하지만 외부 side effect 를 강제 라우팅하지 않는다.
5
+ * 사람과 Host 가 실행한다. Guard 는 아무것도 hard-block 하지 않는다.
6
+ * AUTO ASC-managed Agent 가 자율 실행하고, 외부 write 는 승인된 실행 경로로만 나간다.
7
+ * raw 외부 write 는 Guard 가 막는다.
8
+ *
9
+ * **어느 쪽도 HITL 을 바꾸지 않는다.** AUTO 가 사람의 결정을 대신 승인하지 않고,
10
+ * MANUAL 이 승인을 받은 것으로 만들지도 않는다 (H-03 · H-04).
11
+ */
12
+ export declare const ExecutionMode: z.ZodEnum<["MANUAL", "AUTO"]>;
13
+ export type ExecutionMode = z.infer<typeof ExecutionMode>;
14
+ /** policy scope 안의 키. freeze 정책이 사는 그 자리다 — 새 저장소를 만들지 않는다. */
15
+ export declare const EXECUTION_MODE_KEY = "execution-mode";
16
+ export declare const ExecutionModeRecord: z.ZodObject<{
17
+ mode: z.ZodEnum<["MANUAL", "AUTO"]>;
18
+ /** 언제 정해졌는가. 기록이 없는 workspace 와 명시적으로 정한 workspace 를 가른다. */
19
+ since: z.ZodOptional<z.ZodString>;
20
+ /** 누가 정했는가. AUTO→MANUAL 은 Controller 권한이 필요하다 (§11). */
21
+ by: z.ZodOptional<z.ZodString>;
22
+ }, "strip", z.ZodTypeAny, {
23
+ mode: "MANUAL" | "AUTO";
24
+ since?: string | undefined;
25
+ by?: string | undefined;
26
+ }, {
27
+ mode: "MANUAL" | "AUTO";
28
+ since?: string | undefined;
29
+ by?: string | undefined;
30
+ }>;
31
+ export type ExecutionModeRecord = z.infer<typeof ExecutionModeRecord>;
32
+ /** 기록이 있는데 읽지 못한 이유. **새 mode 값이 아니다** — 읽기의 결과일 뿐이다. */
33
+ export type ModeStateProblem = 'MODE_STATE_UNREADABLE' | 'MODE_STATE_INVALID';
34
+ /**
35
+ * 지금 이 workspace 의 실행 축.
36
+ *
37
+ * mode 는 여전히 MANUAL·AUTO 둘뿐이다. 달라진 것은 **모를 수 있다는 사실을 적는다**는 것:
38
+ *
39
+ * ```text
40
+ * 기록 없음 mode MANUAL · chosen false 아무도 고르지 않았다
41
+ * 기록 있음 mode 그대로 · chosen true 사람이 고른 값이다
42
+ * 기록 있는데 못 읽음 mode 없음 · degraded AUTO 였을 수도 있다 — 모른다
43
+ * ```
44
+ *
45
+ * 마지막 자리를 MANUAL 로 적으면 그것이 fail-open 이다: 저장돼 있던 AUTO 가 파일 손상·
46
+ * 권한·I/O 하나로 조용히 풀린다. 그래서 `mode` 를 비워 두고, 호출자가 그 사실을 마주하게 한다.
47
+ */
48
+ export type ExecutionModeState = {
49
+ mode?: ExecutionMode;
50
+ chosen: boolean;
51
+ since?: string;
52
+ by?: string;
53
+ degraded?: ModeStateProblem;
54
+ };
55
+ /**
56
+ * 이 상태에서 밖으로 나가는 raw 쓰기를 강제 경로로 돌릴 것인가.
57
+ *
58
+ * ```text
59
+ * ENFORCE AUTO 이거나, 기록을 읽지 못했다 (모르는 것을 푸는 쪽으로 기울지 않는다)
60
+ * ADVISE MANUAL 이거나, 아무도 고르지 않았다
61
+ * ```
62
+ *
63
+ * **ENFORCE 는 AUTO 라는 뜻이 아니다.** 읽지 못한 자리는 AUTO 라고 주장하지 않으면서도
64
+ * 열어 두지 않는다 — 그 둘은 다른 말이고, 화면은 그 차이를 그대로 보여 준다.
65
+ */
66
+ export declare function enforcementOf(state: ExecutionModeState): 'ENFORCE' | 'ADVISE';
67
+ /** 사람이 읽는 한 줄. 세 자리를 각각 다르게 말한다. */
68
+ export declare function modeLine(state: ExecutionModeState): string;
69
+ /**
70
+ * 기록이 없으면 AUTO 가 **아니다**.
71
+ *
72
+ * 처음에는 반대로 두었다: 0.7 workspace 가 말없이 보호를 잃지 않게 하려는 것이었다.
73
+ * 그 결정은 이 릴리스의 상위 계약과 충돌한다 — AUTO 는 사람이 고르고 readiness 를 통과한
74
+ * 결과로만 존재할 수 있고(E-01), 기록이 없다는 사실은 그 둘 중 어느 것도 증명하지 않는다.
75
+ * 기록 없이 AUTO 로 읽으면 readiness 를 한 번도 거치지 않은 enforcement 가 켜진다.
76
+ *
77
+ * 그래서 기록이 없는 workspace 는 hard enforcement 없이 돈다. ASC 가 꺼지는 것이 아니다 —
78
+ * 일 관리·결정권·검수·감사는 그대로 살아 있고, Guard 만 강제하지 않는다 (§F).
79
+ */
80
+ export declare const DEFAULT_EXECUTION_MODE: ExecutionMode;
81
+ /**
82
+ * 읽는다. **없는 것과 못 읽은 것을 가른다** (0.8.0 보정 P0-1).
83
+ *
84
+ * 없으면 아무도 고르지 않은 것이고, 못 읽으면 무엇이 저장돼 있었는지 모르는 것이다.
85
+ * 뒤엣것을 MANUAL 로 적는 순간 저장된 AUTO 가 파일 하나로 풀린다.
86
+ */
87
+ export declare function readExecutionMode(scope: ScopedStore): Promise<ExecutionModeState>;
88
+ export declare function writeExecutionMode(scope: ScopedStore, mode: ExecutionMode, by: string | undefined, now?: string): Promise<ExecutionModeRecord>;
89
+ /**
90
+ * AUTO 를 켤 때 물어야 하는 것 — **셋이다.**
91
+ *
92
+ * 한때 아홉이었다. binding·provider·review·verify·controller 까지 activation 시점에
93
+ * 물었는데, 그것들은 행위마다 달라지는 사실이다: 어느 저장소로 가는지, 그 행위를 이
94
+ * 통로가 되돌려 읽을 수 있는지는 **그 행위를 할 때** 답할 질문이고, 실제로 CHECK 단계가
95
+ * 그것을 다시 묻는다. 같은 사실을 두 번 판정하면 둘이 갈릴 자리를 만드는 것이고,
96
+ * "AUTO 를 켜려면 미래의 모든 행위가 지금 가능해야 한다" 는 과장이 된다.
97
+ *
98
+ * 남은 셋은 activation 시점에만 답할 수 있는 것들이다:
99
+ *
100
+ * ```text
101
+ * executor 관리된 쓰기 경로가 조립되는가 — 없으면 AUTO 는 막기만 하는 mode 다
102
+ * guard 막을 것을 실제로 막을 수 있는가 — 없으면 AUTO 는 이름뿐이다
103
+ * control-plane Host 안에서 ASC 명령이 도는가 — 없으면 나갈 문이 없다 (0.7.1 실측)
104
+ * ```
105
+ */
106
+ export declare const READINESS_AXES: readonly ["executor", "guard", "control-plane"];
107
+ export type ReadinessAxisName = (typeof READINESS_AXES)[number];
108
+ /**
109
+ * READY 그대로 쓸 수 있다
110
+ * BLOCKED_BY_HOST Host 정책이 막고 있다 — ASC 가 고칠 수 없는 자리다
111
+ * MISSING 아직 없다
112
+ * DEGRADED 있지만 지금 쓸 수 없다
113
+ * UNKNOWN 확인하지 못했다. 있다고도 없다고도 말하지 않는다
114
+ */
115
+ export type AxisState = 'READY' | 'BLOCKED_BY_HOST' | 'MISSING' | 'DEGRADED' | 'UNKNOWN';
116
+ export type ReadinessAxis = {
117
+ axis: ReadinessAxisName;
118
+ state: AxisState;
119
+ detail?: string;
120
+ };
121
+ export type AutoReadiness = {
122
+ ready: boolean;
123
+ axes: ReadinessAxis[];
124
+ /** AUTO 를 막는 축들. 순서는 §9 의 확인 순서 그대로다. */
125
+ blocking: ReadinessAxis[];
126
+ };
127
+ /**
128
+ * AUTO 를 켜도 되는가.
129
+ *
130
+ * 묻는 것은 하나다 — **AUTO 를 켠 뒤에도 일이 나갈 길과 사람이 나갈 길이 있는가.**
131
+ * 행위 하나하나가 지금 가능한지는 그 행위를 할 때 CHECK 가 답한다.
132
+ *
133
+ * READY 가 아닌 것은 전부 막는다. UNKNOWN 을 READY 로 뭉개면 그것이 곧 나갈 길 없는
134
+ * AUTO 로 들어가는 근거가 된다.
135
+ */
136
+ export declare function judgeAutoReadiness(observed: readonly ReadinessAxis[]): AutoReadiness;
@@ -0,0 +1,127 @@
1
+ // Execution Mode — 실행을 누가 하는가 (0.8.0 Axis C).
2
+ //
3
+ // 세 축은 서로를 대신하지 않는다:
4
+ //
5
+ // Agent Management 누가 무엇을 맡았고 어디까지 왔는가 (Work / Session / Handoff)
6
+ // Decision Authority 이 판단은 사람의 것인가 (Inbox / Approval / Query)
7
+ // Execution Mode 결정된 행위를 누가 실행하는가 ← 이 파일
8
+ //
9
+ // 이 파일에는 mode 하나와 그 mode 로 갈 수 있는지를 판정하는 순수 함수만 있다.
10
+ // state machine 도 approval entity 도 만들지 않는다 — mode 는 기존 policy 저장소의
11
+ // 필드 하나이고, readiness 는 이미 관측되는 사실에서 파생한다.
12
+ import { z } from 'zod';
13
+ /**
14
+ * MANUAL ASC 가 작업·판단·기록을 관리하지만 외부 side effect 를 강제 라우팅하지 않는다.
15
+ * 사람과 Host 가 실행한다. Guard 는 아무것도 hard-block 하지 않는다.
16
+ * AUTO ASC-managed Agent 가 자율 실행하고, 외부 write 는 승인된 실행 경로로만 나간다.
17
+ * raw 외부 write 는 Guard 가 막는다.
18
+ *
19
+ * **어느 쪽도 HITL 을 바꾸지 않는다.** AUTO 가 사람의 결정을 대신 승인하지 않고,
20
+ * MANUAL 이 승인을 받은 것으로 만들지도 않는다 (H-03 · H-04).
21
+ */
22
+ export const ExecutionMode = z.enum(['MANUAL', 'AUTO']);
23
+ /** policy scope 안의 키. freeze 정책이 사는 그 자리다 — 새 저장소를 만들지 않는다. */
24
+ export const EXECUTION_MODE_KEY = 'execution-mode';
25
+ export const ExecutionModeRecord = z.object({
26
+ mode: ExecutionMode,
27
+ /** 언제 정해졌는가. 기록이 없는 workspace 와 명시적으로 정한 workspace 를 가른다. */
28
+ since: z.string().optional(),
29
+ /** 누가 정했는가. AUTO→MANUAL 은 Controller 권한이 필요하다 (§11). */
30
+ by: z.string().optional(),
31
+ });
32
+ /**
33
+ * 이 상태에서 밖으로 나가는 raw 쓰기를 강제 경로로 돌릴 것인가.
34
+ *
35
+ * ```text
36
+ * ENFORCE AUTO 이거나, 기록을 읽지 못했다 (모르는 것을 푸는 쪽으로 기울지 않는다)
37
+ * ADVISE MANUAL 이거나, 아무도 고르지 않았다
38
+ * ```
39
+ *
40
+ * **ENFORCE 는 AUTO 라는 뜻이 아니다.** 읽지 못한 자리는 AUTO 라고 주장하지 않으면서도
41
+ * 열어 두지 않는다 — 그 둘은 다른 말이고, 화면은 그 차이를 그대로 보여 준다.
42
+ */
43
+ export function enforcementOf(state) {
44
+ return state.degraded !== undefined || state.mode === 'AUTO' ? 'ENFORCE' : 'ADVISE';
45
+ }
46
+ /** 사람이 읽는 한 줄. 세 자리를 각각 다르게 말한다. */
47
+ export function modeLine(state) {
48
+ if (state.degraded) {
49
+ return state.degraded === 'MODE_STATE_INVALID'
50
+ ? 'Execution Mode: unreadable — the stored record is not valid. Raw external writes stay blocked until it is fixed.'
51
+ : 'Execution Mode: unreadable — the stored record could not be read. Raw external writes stay blocked until it is fixed.';
52
+ }
53
+ return `Execution Mode: ${state.mode}${state.chosen ? '' : ' (never chosen — nothing is being enforced)'}`;
54
+ }
55
+ /**
56
+ * 기록이 없으면 AUTO 가 **아니다**.
57
+ *
58
+ * 처음에는 반대로 두었다: 0.7 workspace 가 말없이 보호를 잃지 않게 하려는 것이었다.
59
+ * 그 결정은 이 릴리스의 상위 계약과 충돌한다 — AUTO 는 사람이 고르고 readiness 를 통과한
60
+ * 결과로만 존재할 수 있고(E-01), 기록이 없다는 사실은 그 둘 중 어느 것도 증명하지 않는다.
61
+ * 기록 없이 AUTO 로 읽으면 readiness 를 한 번도 거치지 않은 enforcement 가 켜진다.
62
+ *
63
+ * 그래서 기록이 없는 workspace 는 hard enforcement 없이 돈다. ASC 가 꺼지는 것이 아니다 —
64
+ * 일 관리·결정권·검수·감사는 그대로 살아 있고, Guard 만 강제하지 않는다 (§F).
65
+ */
66
+ export const DEFAULT_EXECUTION_MODE = 'MANUAL';
67
+ /**
68
+ * 읽는다. **없는 것과 못 읽은 것을 가른다** (0.8.0 보정 P0-1).
69
+ *
70
+ * 없으면 아무도 고르지 않은 것이고, 못 읽으면 무엇이 저장돼 있었는지 모르는 것이다.
71
+ * 뒤엣것을 MANUAL 로 적는 순간 저장된 AUTO 가 파일 하나로 풀린다.
72
+ */
73
+ export async function readExecutionMode(scope) {
74
+ let raw;
75
+ try {
76
+ raw = await scope.get(EXECUTION_MODE_KEY);
77
+ }
78
+ catch {
79
+ return { chosen: true, degraded: 'MODE_STATE_UNREADABLE' };
80
+ }
81
+ if (raw === null)
82
+ return { mode: DEFAULT_EXECUTION_MODE, chosen: false };
83
+ try {
84
+ return { ...ExecutionModeRecord.parse(JSON.parse(raw)), chosen: true };
85
+ }
86
+ catch {
87
+ return { chosen: true, degraded: 'MODE_STATE_INVALID' };
88
+ }
89
+ }
90
+ export async function writeExecutionMode(scope, mode, by, now = new Date().toISOString()) {
91
+ const record = ExecutionModeRecord.parse({ mode, since: now, ...(by ? { by } : {}) });
92
+ await scope.set(EXECUTION_MODE_KEY, JSON.stringify(record));
93
+ return record;
94
+ }
95
+ /**
96
+ * AUTO 를 켤 때 물어야 하는 것 — **셋이다.**
97
+ *
98
+ * 한때 아홉이었다. binding·provider·review·verify·controller 까지 activation 시점에
99
+ * 물었는데, 그것들은 행위마다 달라지는 사실이다: 어느 저장소로 가는지, 그 행위를 이
100
+ * 통로가 되돌려 읽을 수 있는지는 **그 행위를 할 때** 답할 질문이고, 실제로 CHECK 단계가
101
+ * 그것을 다시 묻는다. 같은 사실을 두 번 판정하면 둘이 갈릴 자리를 만드는 것이고,
102
+ * "AUTO 를 켜려면 미래의 모든 행위가 지금 가능해야 한다" 는 과장이 된다.
103
+ *
104
+ * 남은 셋은 activation 시점에만 답할 수 있는 것들이다:
105
+ *
106
+ * ```text
107
+ * executor 관리된 쓰기 경로가 조립되는가 — 없으면 AUTO 는 막기만 하는 mode 다
108
+ * guard 막을 것을 실제로 막을 수 있는가 — 없으면 AUTO 는 이름뿐이다
109
+ * control-plane Host 안에서 ASC 명령이 도는가 — 없으면 나갈 문이 없다 (0.7.1 실측)
110
+ * ```
111
+ */
112
+ export const READINESS_AXES = ['executor', 'guard', 'control-plane'];
113
+ /**
114
+ * AUTO 를 켜도 되는가.
115
+ *
116
+ * 묻는 것은 하나다 — **AUTO 를 켠 뒤에도 일이 나갈 길과 사람이 나갈 길이 있는가.**
117
+ * 행위 하나하나가 지금 가능한지는 그 행위를 할 때 CHECK 가 답한다.
118
+ *
119
+ * READY 가 아닌 것은 전부 막는다. UNKNOWN 을 READY 로 뭉개면 그것이 곧 나갈 길 없는
120
+ * AUTO 로 들어가는 근거가 된다.
121
+ */
122
+ export function judgeAutoReadiness(observed) {
123
+ const order = new Map(READINESS_AXES.map((axis, index) => [axis, index]));
124
+ const axes = [...observed].sort((a, b) => (order.get(a.axis) ?? 99) - (order.get(b.axis) ?? 99));
125
+ const blocking = axes.filter((axis) => axis.state !== 'READY');
126
+ return { ready: blocking.length === 0, axes, blocking };
127
+ }
@@ -30,8 +30,8 @@ export declare const FreezePolicy: z.ZodObject<{
30
30
  since?: string | undefined;
31
31
  }, {
32
32
  reason?: string | undefined;
33
- frozen?: boolean | undefined;
34
33
  since?: string | undefined;
34
+ frozen?: boolean | undefined;
35
35
  denyRemoteRead?: boolean | undefined;
36
36
  }>;
37
37
  export type FreezePolicy = z.infer<typeof FreezePolicy>;
@@ -48,8 +48,8 @@ export declare const DeferredAction: z.ZodObject<{
48
48
  }, "strip", z.ZodTypeAny, {
49
49
  id: string;
50
50
  action: "remote.read" | "remote.write" | "local.inspect" | "local.implement" | "local.test";
51
- intent: string;
52
51
  basis: string[];
52
+ intent: string;
53
53
  deferredAt: string;
54
54
  grantRef?: string | undefined;
55
55
  }, {