@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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// 이 machine에 stable `asc` 가 있는가 (C-14 §3).
|
|
2
|
+
//
|
|
3
|
+
// **두 가지를 절대 혼동하지 않는다.**
|
|
4
|
+
//
|
|
5
|
+
// ```text
|
|
6
|
+
// bootstrap이 자기 의존성으로 들고 온 runtime 임시다. 프로세스가 끝나면 없다.
|
|
7
|
+
// 이 machine에 전역 설치된 runtime 지속된다. 그것이 `asc` 다.
|
|
8
|
+
// ```
|
|
9
|
+
//
|
|
10
|
+
// npx 캐시나 node_modules 안에 runtime이 보인다고 "이미 설치됐다"고 하면, bootstrap이
|
|
11
|
+
// 끝난 뒤 사용자에게는 아무 명령도 남지 않는다. 그래서 판정은 **전역 설치 조회**로만 한다.
|
|
12
|
+
//
|
|
13
|
+
// 실행물 링크는 npm의 몫이다 (불변식 ⑰). 여기서 PATH도 shell도 고치지 않는다.
|
|
14
|
+
import { RELEASE_VERSION, RUNTIME_PACKAGE } from "./release.js";
|
|
15
|
+
/**
|
|
16
|
+
* 전역 설치 상태를 읽는다. **읽기만 한다** (C-14 §6 detect).
|
|
17
|
+
*
|
|
18
|
+
* `npm ls -g --json` 은 전역 트리만 본다 — bootstrap이 들고 온 자기 의존성은 여기 안 나온다.
|
|
19
|
+
* 그것이 이 함수가 `npm ls` 를 쓰는 이유다.
|
|
20
|
+
*/
|
|
21
|
+
export async function detectStableInstall(run, expectedVersion = RELEASE_VERSION) {
|
|
22
|
+
const listed = await run('npm', ['ls', '-g', '--depth=0', '--json', RUNTIME_PACKAGE]);
|
|
23
|
+
const installedVersion = parseGlobalVersion(listed.stdout);
|
|
24
|
+
// 실행물이 보이는가는 별개 사실이다. node manager에 따라 설치는 됐는데 이 프로세스의
|
|
25
|
+
// PATH에는 없을 수 있고, 그때 "설치됨"이라고만 말하면 사람이 갇힌다 (C-14 §3.3).
|
|
26
|
+
const which = await run(process.platform === 'win32' ? 'where' : 'which', ['asc']);
|
|
27
|
+
const executableVisible = which.ok && which.stdout.trim().length > 0;
|
|
28
|
+
if (!installedVersion) {
|
|
29
|
+
return { status: 'NOT_INSTALLED', expectedVersion, executableVisible };
|
|
30
|
+
}
|
|
31
|
+
if (installedVersion !== expectedVersion) {
|
|
32
|
+
return {
|
|
33
|
+
status: 'VERSION_MISMATCH',
|
|
34
|
+
expectedVersion,
|
|
35
|
+
installedVersion,
|
|
36
|
+
executableVisible,
|
|
37
|
+
detail: `global ${RUNTIME_PACKAGE} is ${installedVersion}, expected ${expectedVersion}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (!executableVisible) {
|
|
41
|
+
return {
|
|
42
|
+
status: 'BROKEN',
|
|
43
|
+
expectedVersion,
|
|
44
|
+
installedVersion,
|
|
45
|
+
executableVisible,
|
|
46
|
+
detail: '`asc` is not visible in this process — npm global prefix or PATH',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return { status: 'CURRENT', expectedVersion, installedVersion, executableVisible };
|
|
50
|
+
}
|
|
51
|
+
/** `npm ls --json` 에서 이 패키지의 버전만 꺼낸다. 못 읽으면 없는 것으로 본다. */
|
|
52
|
+
function parseGlobalVersion(stdout) {
|
|
53
|
+
try {
|
|
54
|
+
const tree = JSON.parse(stdout);
|
|
55
|
+
const version = tree.dependencies?.[RUNTIME_PACKAGE]?.version;
|
|
56
|
+
return typeof version === 'string' ? version : undefined;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 전역에 exact version으로 설치한다. **exact 말고는 쓰지 않는다** (불변식 ⑧).
|
|
64
|
+
*
|
|
65
|
+
* 여기서 조건을 다시 따지지 않는다 — 무엇을 설치할지는 plan이 이미 정했다 (불변식 ⑩).
|
|
66
|
+
*/
|
|
67
|
+
export async function installStableRuntime(run, version = RELEASE_VERSION) {
|
|
68
|
+
const spec = `${RUNTIME_PACKAGE}@${version}`;
|
|
69
|
+
const result = await run('npm', ['install', '-g', spec]);
|
|
70
|
+
return result.ok ? { ok: true } : { ok: false, detail: result.stderr.trim() || result.stdout.trim() };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 설치했다고 끝이 아니다 — exit 0은 "npm이 화내지 않았다"까지다 (C-14 §3.3).
|
|
74
|
+
*
|
|
75
|
+
* 버전이 맞는지, 그리고 **실행물이 실제로 보이는지**까지 본다. 안 보이면 성공으로
|
|
76
|
+
* 뭉개지 않고 새 터미널을 열라고 말한다.
|
|
77
|
+
*/
|
|
78
|
+
export async function verifyStableInstall(run, expectedVersion = RELEASE_VERSION) {
|
|
79
|
+
const state = await detectStableInstall(run, expectedVersion);
|
|
80
|
+
if (state.status === 'CURRENT')
|
|
81
|
+
return { ok: true, state };
|
|
82
|
+
if (state.status === 'BROKEN' && state.installedVersion === expectedVersion) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
state,
|
|
86
|
+
remedy: 'Runtime package was installed, but `asc` is not visible in this process. Open a new terminal and run `asc setup status`.',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return { ok: false, state, remedy: state.detail };
|
|
90
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** 이 패키지의 이름. development checkout이 진짜 ASC인지 여기에 견준다. */
|
|
2
|
+
export declare const RUNTIME_PACKAGE = "@asc-agent/runtime";
|
|
3
|
+
/** build 산출물의 진입점. checkout 뿌리 기준 상대경로다. */
|
|
4
|
+
export declare const RUNTIME_ENTRY: string;
|
|
5
|
+
export declare const SELECTION_FILE = "runtime.json";
|
|
6
|
+
export type RuntimeMode = 'package' | 'development';
|
|
7
|
+
export type RuntimeSelection = {
|
|
8
|
+
version: 1;
|
|
9
|
+
runtime: {
|
|
10
|
+
mode: 'package';
|
|
11
|
+
};
|
|
12
|
+
} | {
|
|
13
|
+
version: 1;
|
|
14
|
+
runtime: {
|
|
15
|
+
mode: 'development';
|
|
16
|
+
source: string;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export type ResolvedTarget =
|
|
20
|
+
/** 설치된 실행물이 스스로 돈다. 중간 프로세스도, network도 없다. */
|
|
21
|
+
{
|
|
22
|
+
kind: 'package';
|
|
23
|
+
}
|
|
24
|
+
/** 지정된 checkout의 build를 대신 실행한다. */
|
|
25
|
+
| {
|
|
26
|
+
kind: 'development';
|
|
27
|
+
source: string;
|
|
28
|
+
entry: string;
|
|
29
|
+
};
|
|
30
|
+
export type ResolveFailure = {
|
|
31
|
+
code: 'ASC_DEVELOPMENT_SOURCE_INVALID';
|
|
32
|
+
detail: string;
|
|
33
|
+
/** 사람이 실행할 한 줄. 무엇이 틀렸는지만 말하고 끝내지 않는다. */
|
|
34
|
+
nextCommand: string;
|
|
35
|
+
/**
|
|
36
|
+
* 이 해법이 **어느 checkout에 대한 것인가**.
|
|
37
|
+
*
|
|
38
|
+
* 지금 cwd가 그 checkout이라는 보장이 없다. `npm run build` 한 줄만 주면 엉뚱한
|
|
39
|
+
* 프로젝트에서 빌드가 돈다 — 실제로 그럴 수 있는 배치다(작업 중인 저장소 ≠ ASC checkout).
|
|
40
|
+
*/
|
|
41
|
+
remediationTarget?: string;
|
|
42
|
+
};
|
|
43
|
+
export declare const selectionPath: (ascHome: string) => string;
|
|
44
|
+
/**
|
|
45
|
+
* 읽지 못하는 선택은 **없는 것으로 본다** (C-14 §5).
|
|
46
|
+
*
|
|
47
|
+
* 반쯤 해석해서 엉뚱한 build를 부르는 것보다, 선택이 없다고 말하고 기본으로 도는 것이 낫다.
|
|
48
|
+
*/
|
|
49
|
+
export declare function readRuntimeSelection(ascHome: string): Promise<RuntimeSelection | undefined>;
|
|
50
|
+
/** 손으로 고칠 수 있는 파일이다. 형식이 어긋나면 받아들이지 않는다. */
|
|
51
|
+
export declare function normalizeSelection(value: unknown): RuntimeSelection | undefined;
|
|
52
|
+
/** tmp+rename — 중간에 끊겨도 반쯤 쓰인 선택이 남지 않는다. */
|
|
53
|
+
export declare function writeRuntimeSelection(ascHome: string, selection: RuntimeSelection): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* 어느 build를 부를 것인가. **거절은 여기서 끝낸다** (C-14 §4.1).
|
|
56
|
+
*
|
|
57
|
+
* 경로가 틀렸거나 build가 없는 것을 통과시키면 사람은 한참 뒤에 module-not-found를
|
|
58
|
+
* 맞는다. 무엇이 없는지 아는 곳에서 말하는 것이 싸다.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveRuntimeTarget(selection: RuntimeSelection | undefined): Promise<ResolvedTarget | ResolveFailure>;
|
|
61
|
+
/** 사람이 읽는 한 줄. 지금 무엇이 돌지가 먼저다. */
|
|
62
|
+
export declare function runtimeSelectionLine(target: ResolvedTarget | ResolveFailure): string;
|
|
63
|
+
/**
|
|
64
|
+
* 해법을 사람에게 말한다. **무엇에 대한 해법인지 먼저 말한다** (C-14 §3.4의 태도).
|
|
65
|
+
*
|
|
66
|
+
* `npm run build` 만 던지면 지금 열려 있는 저장소에서 그것을 실행할 사람이 있다.
|
|
67
|
+
*/
|
|
68
|
+
export declare function remediationLines(failure: ResolveFailure): string[];
|
|
69
|
+
/** agent가 그대로 소비하는 형태. 어느 checkout인지가 데이터로 들어 있다. */
|
|
70
|
+
export declare function remediationAction(failure: ResolveFailure): {
|
|
71
|
+
type: 'build_development_runtime' | 'select_development_runtime';
|
|
72
|
+
source?: string;
|
|
73
|
+
display: string;
|
|
74
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// 어느 build를 부를 것인가 (C-14 §4·§5).
|
|
2
|
+
//
|
|
3
|
+
// **이것은 C-12의 "always-on runtime"과 다른 축이다.** 저쪽은 상태가 어떻게 지속되는가고,
|
|
4
|
+
// 여기는 실행물이 어느 build인가다. 같은 낱말을 쓰지만 섞으면 안 된다.
|
|
5
|
+
//
|
|
6
|
+
// 이 파일은 선택만 안다 — credential·project key·workspace state는 여기 오지 않는다.
|
|
7
|
+
// 그리고 **선택을 바꾸는 것은 project를 바꾸는 것이 아니다** (불변식 ⑤).
|
|
8
|
+
import { constants } from 'node:fs';
|
|
9
|
+
import { access, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
/** 이 패키지의 이름. development checkout이 진짜 ASC인지 여기에 견준다. */
|
|
12
|
+
export const RUNTIME_PACKAGE = '@asc-agent/runtime';
|
|
13
|
+
/** build 산출물의 진입점. checkout 뿌리 기준 상대경로다. */
|
|
14
|
+
export const RUNTIME_ENTRY = join('dist', 'cli', 'asc.js');
|
|
15
|
+
export const SELECTION_FILE = 'runtime.json';
|
|
16
|
+
const exists = async (path) => {
|
|
17
|
+
try {
|
|
18
|
+
await access(path, constants.F_OK);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
export const selectionPath = (ascHome) => join(ascHome, SELECTION_FILE);
|
|
26
|
+
/**
|
|
27
|
+
* 읽지 못하는 선택은 **없는 것으로 본다** (C-14 §5).
|
|
28
|
+
*
|
|
29
|
+
* 반쯤 해석해서 엉뚱한 build를 부르는 것보다, 선택이 없다고 말하고 기본으로 도는 것이 낫다.
|
|
30
|
+
*/
|
|
31
|
+
export async function readRuntimeSelection(ascHome) {
|
|
32
|
+
let raw;
|
|
33
|
+
try {
|
|
34
|
+
raw = await readFile(selectionPath(ascHome), 'utf8');
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return normalizeSelection(JSON.parse(raw));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** 손으로 고칠 수 있는 파일이다. 형식이 어긋나면 받아들이지 않는다. */
|
|
47
|
+
export function normalizeSelection(value) {
|
|
48
|
+
if (typeof value !== 'object' || value === null)
|
|
49
|
+
return undefined;
|
|
50
|
+
const record = value;
|
|
51
|
+
if (record.version !== 1)
|
|
52
|
+
return undefined;
|
|
53
|
+
const runtime = record.runtime;
|
|
54
|
+
if (!runtime)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (runtime.mode === 'package')
|
|
57
|
+
return { version: 1, runtime: { mode: 'package' } };
|
|
58
|
+
if (runtime.mode === 'development') {
|
|
59
|
+
const source = runtime.source;
|
|
60
|
+
if (typeof source !== 'string' || source.trim() === '')
|
|
61
|
+
return undefined;
|
|
62
|
+
return { version: 1, runtime: { mode: 'development', source } };
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
/** tmp+rename — 중간에 끊겨도 반쯤 쓰인 선택이 남지 않는다. */
|
|
67
|
+
export async function writeRuntimeSelection(ascHome, selection) {
|
|
68
|
+
const target = selectionPath(ascHome);
|
|
69
|
+
await mkdir(dirname(target), { recursive: true });
|
|
70
|
+
const tmp = `${target}.tmp`;
|
|
71
|
+
await writeFile(tmp, `${JSON.stringify(selection, null, 2)}\n`, 'utf8');
|
|
72
|
+
await rename(tmp, target);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 어느 build를 부를 것인가. **거절은 여기서 끝낸다** (C-14 §4.1).
|
|
76
|
+
*
|
|
77
|
+
* 경로가 틀렸거나 build가 없는 것을 통과시키면 사람은 한참 뒤에 module-not-found를
|
|
78
|
+
* 맞는다. 무엇이 없는지 아는 곳에서 말하는 것이 싸다.
|
|
79
|
+
*/
|
|
80
|
+
export async function resolveRuntimeTarget(selection) {
|
|
81
|
+
// 선택이 없으면 지금 도는 실행물이 곧 답이다 — 첫 실행이 설정을 요구하지 않는다.
|
|
82
|
+
if (!selection || selection.runtime.mode === 'package')
|
|
83
|
+
return { kind: 'package' };
|
|
84
|
+
const source = selection.runtime.source;
|
|
85
|
+
const reject = (detail) => ({
|
|
86
|
+
code: 'ASC_DEVELOPMENT_SOURCE_INVALID',
|
|
87
|
+
detail,
|
|
88
|
+
nextCommand: `asc runtime use development ${source}`,
|
|
89
|
+
});
|
|
90
|
+
if (!(await exists(source)))
|
|
91
|
+
return reject(`${source} does not exist — it was moved or removed`);
|
|
92
|
+
const manifest = join(source, 'package.json');
|
|
93
|
+
if (!(await exists(manifest)))
|
|
94
|
+
return reject(`${source} has no package.json`);
|
|
95
|
+
let name;
|
|
96
|
+
try {
|
|
97
|
+
name = JSON.parse(await readFile(manifest, 'utf8')).name;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return reject(`${manifest} could not be read`);
|
|
101
|
+
}
|
|
102
|
+
if (name !== RUNTIME_PACKAGE) {
|
|
103
|
+
return reject(`${source} is not ${RUNTIME_PACKAGE} (found ${String(name)})`);
|
|
104
|
+
}
|
|
105
|
+
const entry = join(source, RUNTIME_ENTRY);
|
|
106
|
+
if (!(await exists(entry))) {
|
|
107
|
+
return {
|
|
108
|
+
code: 'ASC_DEVELOPMENT_SOURCE_INVALID',
|
|
109
|
+
detail: `${source} has not been built yet`,
|
|
110
|
+
// 이 경우만 다르다 — 경로는 맞고 build만 없다. **어느 checkout인지 함께 든다**:
|
|
111
|
+
// 지금 cwd가 그곳이라고 가정하면 남의 프로젝트를 빌드하게 만든다.
|
|
112
|
+
nextCommand: 'npm run build',
|
|
113
|
+
remediationTarget: source,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { kind: 'development', source, entry };
|
|
117
|
+
}
|
|
118
|
+
/** 사람이 읽는 한 줄. 지금 무엇이 돌지가 먼저다. */
|
|
119
|
+
export function runtimeSelectionLine(target) {
|
|
120
|
+
if ('code' in target)
|
|
121
|
+
return `Runtime selection does not resolve — ${target.detail}`;
|
|
122
|
+
return target.kind === 'package'
|
|
123
|
+
? 'runtime: package (the installed executable)'
|
|
124
|
+
: `runtime: development (${target.source})`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 해법을 사람에게 말한다. **무엇에 대한 해법인지 먼저 말한다** (C-14 §3.4의 태도).
|
|
128
|
+
*
|
|
129
|
+
* `npm run build` 만 던지면 지금 열려 있는 저장소에서 그것을 실행할 사람이 있다.
|
|
130
|
+
*/
|
|
131
|
+
export function remediationLines(failure) {
|
|
132
|
+
const lines = [`${failure.code}: ${failure.detail}`];
|
|
133
|
+
if (failure.remediationTarget) {
|
|
134
|
+
lines.push(` target: ${failure.remediationTarget}`);
|
|
135
|
+
lines.push(` Build that checkout and retry — do not run this in the current directory.`);
|
|
136
|
+
}
|
|
137
|
+
lines.push(` Run: ${failure.nextCommand}`);
|
|
138
|
+
return lines;
|
|
139
|
+
}
|
|
140
|
+
/** agent가 그대로 소비하는 형태. 어느 checkout인지가 데이터로 들어 있다. */
|
|
141
|
+
export function remediationAction(failure) {
|
|
142
|
+
return failure.remediationTarget
|
|
143
|
+
? {
|
|
144
|
+
type: 'build_development_runtime',
|
|
145
|
+
source: failure.remediationTarget,
|
|
146
|
+
display: failure.nextCommand,
|
|
147
|
+
}
|
|
148
|
+
: { type: 'select_development_runtime', display: failure.nextCommand };
|
|
149
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ExecutionGrant } from '../model/entities.ts';
|
|
2
|
+
import type { ScmPort } from '../../ports/scm.ts';
|
|
3
|
+
import type { StateStore } from '../../ports/state-store.ts';
|
|
4
|
+
export type ExecuteOutcome = {
|
|
5
|
+
ok: true;
|
|
6
|
+
grant: ExecutionGrant;
|
|
7
|
+
resultRef: string;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
reason: 'NOT_FOUND';
|
|
11
|
+
}
|
|
12
|
+
/** 이미 누군가 집었거나 끝난 Grant. 재실행이 막히는 지점이다. */
|
|
13
|
+
| {
|
|
14
|
+
ok: false;
|
|
15
|
+
reason: 'NOT_CLAIMABLE';
|
|
16
|
+
status: ExecutionGrant['status'];
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
reason: 'CLAIMED_BY_OTHER';
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
reason: 'EXPIRED';
|
|
23
|
+
}
|
|
24
|
+
/** 계약이 허용하지 않는 행위다 — 계약서와 다른 일을 하려는 것이므로 실행하지 않는다. */
|
|
25
|
+
| {
|
|
26
|
+
ok: false;
|
|
27
|
+
reason: 'FORBIDDEN_ACTION';
|
|
28
|
+
detail: string;
|
|
29
|
+
}
|
|
30
|
+
/** 승인 이후 대상이 움직였다 — 실행하지 않고 되돌린다. */
|
|
31
|
+
| {
|
|
32
|
+
ok: false;
|
|
33
|
+
reason: 'DRIFT';
|
|
34
|
+
detail: string;
|
|
35
|
+
} | {
|
|
36
|
+
ok: false;
|
|
37
|
+
reason: 'ACTION_FAILED';
|
|
38
|
+
detail: string;
|
|
39
|
+
};
|
|
40
|
+
export type ExecutorDeps = {
|
|
41
|
+
store: StateStore;
|
|
42
|
+
scm: ScmPort;
|
|
43
|
+
/** 이 Physical Run의 식별자. 누가 집었는지 Grant에 남는다. */
|
|
44
|
+
runId: string;
|
|
45
|
+
now?: () => string;
|
|
46
|
+
};
|
|
47
|
+
export declare class Executor {
|
|
48
|
+
#private;
|
|
49
|
+
constructor(deps: ExecutorDeps);
|
|
50
|
+
run(grantId: string): Promise<ExecuteOutcome>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Ephemeral Executor — Grant 하나를 집어 외부 행위 한 번을 수행하고 끝난다.
|
|
2
|
+
//
|
|
3
|
+
// 이 파일이 시스템에서 유일하게 외부 write를 부르는 지점이다. 순서가 곧 안전장치다:
|
|
4
|
+
//
|
|
5
|
+
// CLAIM (원자적) → 계약 범위 확인 → Drift Guard → 외부 행위 1회 → EXECUTED
|
|
6
|
+
//
|
|
7
|
+
// CLAIM을 먼저 하는 이유는 두 Executor가 같은 Grant로 같은 댓글을 두 번 달지 않게 하기
|
|
8
|
+
// 위해서고, Drift Guard를 그 다음에 두는 이유는 승인 이후 스레드가 움직였을 때 오래된
|
|
9
|
+
// 초안이 나가지 않게 하기 위해서다 (OM §11.9). 둘 다 실패는 조용히 넘어가지 않는다.
|
|
10
|
+
import { transitionGrant, transitionRequest } from "../model/transitions.js";
|
|
11
|
+
import { applyTransition } from "../runtime/store-ops.js";
|
|
12
|
+
export class Executor {
|
|
13
|
+
#store;
|
|
14
|
+
#scm;
|
|
15
|
+
#runId;
|
|
16
|
+
#now;
|
|
17
|
+
constructor(deps) {
|
|
18
|
+
this.#store = deps.store;
|
|
19
|
+
this.#scm = deps.scm;
|
|
20
|
+
this.#runId = deps.runId;
|
|
21
|
+
this.#now = deps.now ?? (() => new Date().toISOString());
|
|
22
|
+
}
|
|
23
|
+
async run(grantId) {
|
|
24
|
+
const grant = await this.#store.get('grant', grantId);
|
|
25
|
+
if (!grant)
|
|
26
|
+
return { ok: false, reason: 'NOT_FOUND' };
|
|
27
|
+
if (grant.status !== 'READY')
|
|
28
|
+
return { ok: false, reason: 'NOT_CLAIMABLE', status: grant.status };
|
|
29
|
+
const at = this.#now();
|
|
30
|
+
if (grant.expiresAt && grant.expiresAt <= at) {
|
|
31
|
+
await this.#close(grant.id, 'EXPIRED', at, '만료');
|
|
32
|
+
return { ok: false, reason: 'EXPIRED' };
|
|
33
|
+
}
|
|
34
|
+
// 1. CLAIM — 두 Run이 동시에 들어와도 하나만 통과한다
|
|
35
|
+
const claimed = await applyTransition(this.#store, 'grant', grant.id, (g) => transitionGrant(g, 'CLAIMED', 'executor', { claimedBy: this.#runId }));
|
|
36
|
+
if (!claimed.ok) {
|
|
37
|
+
if (claimed.reason === 'NOT_FOUND')
|
|
38
|
+
return { ok: false, reason: 'NOT_FOUND' };
|
|
39
|
+
return { ok: false, reason: 'CLAIMED_BY_OTHER' };
|
|
40
|
+
}
|
|
41
|
+
// 2. 계약 범위 확인 — 허용 목록에 없는 행위는 하지 않는다 (fail-closed).
|
|
42
|
+
// 계약 자체가 모순이면 외부 상태를 조회할 이유도 없으므로 Drift Guard보다 앞에 둔다.
|
|
43
|
+
if (!claimed.entity.allowedWrites.includes(claimed.entity.action)) {
|
|
44
|
+
const detail = `'${claimed.entity.action}' is not in allowed writes [${claimed.entity.allowedWrites.join(', ')}]`;
|
|
45
|
+
await this.#close(grant.id, 'INVALIDATED', this.#now(), detail);
|
|
46
|
+
return { ok: false, reason: 'FORBIDDEN_ACTION', detail };
|
|
47
|
+
}
|
|
48
|
+
// 3. Drift Guard — 승인 시점의 기준선과 지금을 대조한다
|
|
49
|
+
const drift = await this.#detectDrift(claimed.entity);
|
|
50
|
+
if (drift) {
|
|
51
|
+
await this.#close(grant.id, 'INVALIDATED', this.#now(), drift);
|
|
52
|
+
return { ok: false, reason: 'DRIFT', detail: drift };
|
|
53
|
+
}
|
|
54
|
+
// 4. 외부 행위 1회. payload는 승인된 내용 그대로 나간다
|
|
55
|
+
const result = await this.#scm.execute({
|
|
56
|
+
action: claimed.entity.action,
|
|
57
|
+
target: claimed.entity.target,
|
|
58
|
+
payload: claimed.entity.payload,
|
|
59
|
+
});
|
|
60
|
+
if (!result.ok) {
|
|
61
|
+
// 재시도하지 않는다. 실패한 호출이 정말 나가지 않았는지는 여기서 알 수 없고,
|
|
62
|
+
// 모른 채 다시 부르면 같은 글이 두 번 올라간다. 사람이 확인하고 새 Grant를 낸다.
|
|
63
|
+
await this.#close(grant.id, 'INVALIDATED', this.#now(), `실행 실패: ${result.error}`);
|
|
64
|
+
return { ok: false, reason: 'ACTION_FAILED', detail: result.error };
|
|
65
|
+
}
|
|
66
|
+
// 5. 소비 기록 — 성공한 Grant는 다시 쓸 수 없다
|
|
67
|
+
const executedAt = this.#now();
|
|
68
|
+
const executed = await applyTransition(this.#store, 'grant', grant.id, (g) => transitionGrant(g, 'EXECUTED', 'executor', { resultRef: result.resultRef, consumedAt: executedAt }));
|
|
69
|
+
if (!executed.ok)
|
|
70
|
+
return { ok: false, reason: 'CLAIMED_BY_OTHER' };
|
|
71
|
+
// 6. 요청을 닫는다. 외부에 무엇이 남았는지 요청에서 바로 따라갈 수 있어야 한다
|
|
72
|
+
await applyTransition(this.#store, 'request', claimed.entity.requestId, (r) => transitionRequest(r, 'DONE', 'executor', { resultRef: result.resultRef }));
|
|
73
|
+
await this.#store.appendHistory({
|
|
74
|
+
at: executedAt,
|
|
75
|
+
actor: this.#runId,
|
|
76
|
+
kind: 'external_action',
|
|
77
|
+
ref: grant.id,
|
|
78
|
+
detail: `${grant.action} → ${grant.target} = ${result.resultRef}`,
|
|
79
|
+
});
|
|
80
|
+
return { ok: true, grant: executed.entity, resultRef: result.resultRef };
|
|
81
|
+
}
|
|
82
|
+
/** 무엇이 달라졌는지 문장으로 돌려준다. 달라진 것이 없으면 null. */
|
|
83
|
+
async #detectDrift(grant) {
|
|
84
|
+
if (grant.threadLastEventId !== undefined) {
|
|
85
|
+
const thread = await this.#scm.getThread(grant.target);
|
|
86
|
+
if (thread.missing)
|
|
87
|
+
return `대상 스레드를 찾을 수 없다 (${grant.target})`;
|
|
88
|
+
if (thread.lastEventId !== grant.threadLastEventId) {
|
|
89
|
+
return `스레드에 새 이벤트가 있다 (${grant.threadLastEventId} → ${thread.lastEventId})`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (grant.snapshot.length > 0) {
|
|
93
|
+
const current = await this.#scm.getBaselines(grant.snapshot.map((s) => ({ sourceId: s.sourceId })));
|
|
94
|
+
for (const now of current) {
|
|
95
|
+
const before = grant.snapshot.find((s) => s.sourceId === now.sourceId)?.baseline;
|
|
96
|
+
if (before !== now.baseline)
|
|
97
|
+
return `정본이 바뀌었다 (${now.sourceId}: ${before} → ${now.baseline})`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
async #close(grantId, to, at, detail) {
|
|
103
|
+
await applyTransition(this.#store, 'grant', grantId, (g) => transitionGrant(g, to, 'executor'));
|
|
104
|
+
await this.#store.appendHistory({ at, actor: this.#runId, kind: `grant_${to.toLowerCase()}`, ref: grantId, detail });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ExecutionGrant } from '../model/entities.ts';
|
|
2
|
+
import type { IdentityBinding } from '../../ports/approval.ts';
|
|
3
|
+
import type { StateStore } from '../../ports/state-store.ts';
|
|
4
|
+
export type IssueGrantInput = {
|
|
5
|
+
grantId: string;
|
|
6
|
+
requestId: string;
|
|
7
|
+
/** 발급자. 그 요청의 승인 권한자로 매핑돼 있어야 한다 — 임의 문자열로는 발급되지 않는다. */
|
|
8
|
+
issuedBy: string;
|
|
9
|
+
/** 발급이 들어온 표면. Identity 검증은 채널까지 함께 본다. */
|
|
10
|
+
channel: string;
|
|
11
|
+
/** '<adapter>.<행위>' 형태의 행위 키. Adapter가 해석한다. */
|
|
12
|
+
action: string;
|
|
13
|
+
target: string;
|
|
14
|
+
expiresAt?: string;
|
|
15
|
+
/** 이 계약으로 허용되는 행위 목록. 비우면 `action` 하나만 허용된다. */
|
|
16
|
+
allowedWrites?: string[];
|
|
17
|
+
issuedAt: string;
|
|
18
|
+
};
|
|
19
|
+
export type IssueFailure = {
|
|
20
|
+
kind: 'REQUEST_NOT_FOUND';
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'NOT_APPROVED';
|
|
23
|
+
status: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'FORBIDDEN_ISSUER';
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'NO_PAYLOAD';
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'GRANT_EXISTS';
|
|
30
|
+
};
|
|
31
|
+
export type IssueResult = {
|
|
32
|
+
ok: true;
|
|
33
|
+
grant: ExecutionGrant;
|
|
34
|
+
} | {
|
|
35
|
+
ok: false;
|
|
36
|
+
failure: IssueFailure;
|
|
37
|
+
};
|
|
38
|
+
export declare class GrantService {
|
|
39
|
+
#private;
|
|
40
|
+
constructor(store: StateStore, identity: IdentityBinding);
|
|
41
|
+
issue(input: IssueGrantInput): Promise<IssueResult>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Execution Grant 발급 — 승인된 요청 하나를 밖으로 내보낼 수 있는 일회용 계약을 만든다.
|
|
2
|
+
//
|
|
3
|
+
// 승인은 게시 권한이 아니다 (OM §11.8). APPROVED는 "사람이 내용에 동의했다"까지이고,
|
|
4
|
+
// 실제로 무엇을 어디에 쓸 것인지는 Controller가 별도로 지정한다. 그래서 Grant는 Policy
|
|
5
|
+
// hierarchy의 예외가 아니라 그 바깥에서 새로 만들어지는 계약이다 (OM §5.2·§11.5).
|
|
6
|
+
//
|
|
7
|
+
// 승인 즉시 자동 발급하지 않는 이유도 같다. 두 행위를 붙여 놓으면 "승인했으니 나갔겠지"가
|
|
8
|
+
// 되고, 무엇이 언제 나갔는지 사람이 따로 붙잡을 지점이 사라진다.
|
|
9
|
+
import { ExecutionGrant } from "../model/entities.js";
|
|
10
|
+
export class GrantService {
|
|
11
|
+
#store;
|
|
12
|
+
#identity;
|
|
13
|
+
constructor(store, identity) {
|
|
14
|
+
this.#store = store;
|
|
15
|
+
this.#identity = identity;
|
|
16
|
+
}
|
|
17
|
+
async issue(input) {
|
|
18
|
+
const request = await this.#store.get('request', input.requestId);
|
|
19
|
+
if (!request)
|
|
20
|
+
return { ok: false, failure: { kind: 'REQUEST_NOT_FOUND' } };
|
|
21
|
+
if (request.status !== 'APPROVED') {
|
|
22
|
+
return { ok: false, failure: { kind: 'NOT_APPROVED', status: request.status } };
|
|
23
|
+
}
|
|
24
|
+
// 발급도 Controller의 행위다. 승인만 검증하고 발급을 열어 두면 승인 이후 구간이
|
|
25
|
+
// 통째로 무방비가 된다 — 정작 외부로 나가는 권한은 여기서 만들어지기 때문이다.
|
|
26
|
+
const authorized = await this.#identity.verify({
|
|
27
|
+
channel: input.channel,
|
|
28
|
+
actor: input.issuedBy,
|
|
29
|
+
authorizedApprover: request.authorizedApprover,
|
|
30
|
+
});
|
|
31
|
+
if (!authorized) {
|
|
32
|
+
await this.#store.appendHistory({
|
|
33
|
+
at: input.issuedAt,
|
|
34
|
+
actor: input.issuedBy,
|
|
35
|
+
kind: 'grant_rejected',
|
|
36
|
+
ref: input.requestId,
|
|
37
|
+
detail: `unauthorized issuer via ${input.channel} (${input.action})`,
|
|
38
|
+
});
|
|
39
|
+
return { ok: false, failure: { kind: 'FORBIDDEN_ISSUER' } };
|
|
40
|
+
}
|
|
41
|
+
// 내보낼 내용은 승인된 것에서만 나온다. 호출자가 payload를 주입할 수 있으면
|
|
42
|
+
// 사람이 본 적 없는 글이 사람의 승인 기록을 달고 나갈 수 있다.
|
|
43
|
+
// 다른 내용을 보내려면 새 Decision을 받아야 한다.
|
|
44
|
+
const payload = request.decision?.revision ?? request.draft;
|
|
45
|
+
if (payload === undefined)
|
|
46
|
+
return { ok: false, failure: { kind: 'NO_PAYLOAD' } };
|
|
47
|
+
const grant = ExecutionGrant.parse({
|
|
48
|
+
id: input.grantId,
|
|
49
|
+
version: 0,
|
|
50
|
+
requestId: request.id,
|
|
51
|
+
status: 'READY',
|
|
52
|
+
issuedBy: input.issuedBy,
|
|
53
|
+
issuedAt: input.issuedAt,
|
|
54
|
+
...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
|
|
55
|
+
singleUse: true,
|
|
56
|
+
action: input.action,
|
|
57
|
+
target: input.target,
|
|
58
|
+
payload,
|
|
59
|
+
// 이 시점의 정본이 Drift Guard의 기준선이 된다 (OM §11.9)
|
|
60
|
+
snapshot: request.snapshot,
|
|
61
|
+
...(request.source.threadLastEventId !== undefined
|
|
62
|
+
? { threadLastEventId: request.source.threadLastEventId }
|
|
63
|
+
: {}),
|
|
64
|
+
allowedWrites: input.allowedWrites ?? [input.action],
|
|
65
|
+
});
|
|
66
|
+
const created = await this.#store.create('grant', grant);
|
|
67
|
+
if (!created.ok)
|
|
68
|
+
return { ok: false, failure: { kind: 'GRANT_EXISTS' } };
|
|
69
|
+
await this.#store.appendHistory({
|
|
70
|
+
at: input.issuedAt,
|
|
71
|
+
actor: input.issuedBy,
|
|
72
|
+
kind: 'grant_issued',
|
|
73
|
+
ref: grant.id,
|
|
74
|
+
detail: `${grant.action} → ${grant.target} (request ${grant.requestId})`,
|
|
75
|
+
});
|
|
76
|
+
return { ok: true, grant: created.entity };
|
|
77
|
+
}
|
|
78
|
+
}
|