@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,65 @@
|
|
|
1
|
+
// JAM Incremental EventSource — 빠른 변경 회수 (C-07 §1.1).
|
|
2
|
+
//
|
|
3
|
+
// **푸시가 있다고 말하지 않는다.** JAM에는 webhook도 알림 스트림도 없다. 여기서 하는 것은
|
|
4
|
+
// `updated >= watermark` 조회이며, 그것은 C-07 §1.1이 Delta에 명시적으로 포함한 형태다
|
|
5
|
+
// ("webhook / incremental polling / provider notification / **updated-since**").
|
|
6
|
+
//
|
|
7
|
+
// 그래서 이 파일이 여는 것은 `observe.delta` 이지 push가 아니다. 둘을 같은 말로 쓰면
|
|
8
|
+
// "실시간이다"라는 잘못된 기대가 생기고, 그 기대 위에서 사람들이 감시 주기를 늘린다.
|
|
9
|
+
//
|
|
10
|
+
// 시각 정밀도가 핵심 위험이다. JAM의 `updated` 는 분 단위라, 같은 분 안에서 일어난
|
|
11
|
+
// 변경은 watermark를 그 값으로 올리는 순간 영영 안 보인다. **누락이 중복보다 위험하므로**
|
|
12
|
+
// 겹쳐 읽는다 — 중복은 dedupe가 거르지만 누락은 아무도 모른다.
|
|
13
|
+
/**
|
|
14
|
+
* 분 단위 정밀도를 덮는 겹침. JamInventory의 JQL 변환이 쓰는 값과 같은 이유로 60초다 —
|
|
15
|
+
* 한쪽만 겹치면 겹치지 않은 쪽이 조용히 놓친다.
|
|
16
|
+
*/
|
|
17
|
+
export const OVERLAP_MS = 60_000;
|
|
18
|
+
export class JamEventSource {
|
|
19
|
+
id;
|
|
20
|
+
#inventory;
|
|
21
|
+
#now;
|
|
22
|
+
constructor(deps) {
|
|
23
|
+
this.id = deps.id ?? 'jam-updated';
|
|
24
|
+
this.#inventory = deps.inventory;
|
|
25
|
+
this.#now = deps.now ?? (() => new Date().toISOString());
|
|
26
|
+
}
|
|
27
|
+
/** "지금부터 보겠다". 처음 붙인 프로젝트의 과거를 통째로 긁으면 그 자체가 잡음이다. */
|
|
28
|
+
cursorFrom(since) {
|
|
29
|
+
return since;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* cursor 이후로 바뀐 것을 사건으로 흘린다.
|
|
33
|
+
*
|
|
34
|
+
* **cursor는 성공한 회차에만 전진한다.** 목록을 끝까지 못 봤으면(`complete=false`)
|
|
35
|
+
* 그 회차의 최대 시각으로 올리지 않는다 — 못 본 구간을 본 것으로 표시하는 셈이 된다
|
|
36
|
+
* (C-07 §1.5).
|
|
37
|
+
*/
|
|
38
|
+
async drain(cursor) {
|
|
39
|
+
const since = cursor ?? undefined;
|
|
40
|
+
const page = await this.#inventory.enumerate(since ? { updatedSince: overlapped(since) } : {});
|
|
41
|
+
const events = page.items.map((item) => ({
|
|
42
|
+
// 같은 항목이 같은 시각으로 다시 와도 같은 키다 — 중복은 여기서 이미 접힌다
|
|
43
|
+
eventKey: `jam:${item.reference}:${item.revisionMarker}`,
|
|
44
|
+
detectedAt: item.updatedAt,
|
|
45
|
+
reference: item.reference,
|
|
46
|
+
hints: {
|
|
47
|
+
...(item.assignees ? { actors: item.assignees } : {}),
|
|
48
|
+
...(item.labels ? { labels: item.labels } : {}),
|
|
49
|
+
},
|
|
50
|
+
raw: { kind: 'work_item', state: item.state, title: item.title },
|
|
51
|
+
}));
|
|
52
|
+
if (!page.complete) {
|
|
53
|
+
// 못 본 구간이 있다. cursor를 그대로 두어 다음 회차가 같은 자리에서 다시 본다.
|
|
54
|
+
return { events, cursor: cursor ?? null, hasMore: true };
|
|
55
|
+
}
|
|
56
|
+
const latest = page.items.reduce((max, item) => (max === undefined || item.updatedAt > max ? item.updatedAt : max), undefined);
|
|
57
|
+
// 이번에 아무것도 없었으면 시계를 옮기지 않는다 — 옮길 근거가 없다.
|
|
58
|
+
return { events, cursor: latest ?? cursor ?? this.#now(), hasMore: false };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** watermark를 뒤로 물린다. 분 단위 반올림으로 같은 분의 변경을 놓치지 않기 위해서다. */
|
|
62
|
+
function overlapped(since) {
|
|
63
|
+
const at = new Date(since).getTime();
|
|
64
|
+
return Number.isNaN(at) ? since : new Date(at - OVERLAP_MS).toISOString();
|
|
65
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
|
+
export type McpRequest = {
|
|
3
|
+
method: string;
|
|
4
|
+
params?: unknown;
|
|
5
|
+
};
|
|
6
|
+
export type McpFailure = {
|
|
7
|
+
kind: 'SPAWN_FAILED' | 'TIMEOUT' | 'PROCESS_EXITED' | 'PROTOCOL_ERROR' | 'TOOL_ERROR';
|
|
8
|
+
detail: string;
|
|
9
|
+
};
|
|
10
|
+
export type McpResult<T> = {
|
|
11
|
+
ok: true;
|
|
12
|
+
value: T;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
} & McpFailure;
|
|
16
|
+
/** MCP tool 하나의 응답. JAM은 결과를 text content 한 덩어리에 담아 준다. */
|
|
17
|
+
export type ToolResponse = {
|
|
18
|
+
content?: {
|
|
19
|
+
type: string;
|
|
20
|
+
text?: string;
|
|
21
|
+
}[];
|
|
22
|
+
isError?: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type JamMcpClientDeps = {
|
|
25
|
+
/** JAM 실행 경로. 값을 문서·Profile·repo에 남기지 않는다 — 환경에서만 온다. */
|
|
26
|
+
command: string;
|
|
27
|
+
args?: readonly string[];
|
|
28
|
+
/** JAM은 프로젝트 선언(.jira-agent/project.yaml)이 있는 디렉터리에서 떠야 한다. */
|
|
29
|
+
cwd?: string;
|
|
30
|
+
/** 한 요청이 이보다 오래 걸리면 끊는다. 매달린 자식이 회차를 통째로 잡아먹지 않게. */
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
/**
|
|
33
|
+
* 프로세스를 띄우는 통로. 테스트가 실제 자식을 띄우지 않기 위한 주입점이다 —
|
|
34
|
+
* 실 프로세스에 기대는 테스트는 남의 사정으로 깨진다.
|
|
35
|
+
*/
|
|
36
|
+
spawnProcess?: () => ChildProcessWithoutNullStreams;
|
|
37
|
+
};
|
|
38
|
+
export declare class JamMcpClient {
|
|
39
|
+
#private;
|
|
40
|
+
constructor(deps: JamMcpClientDeps);
|
|
41
|
+
/** 띄우고 악수까지. 여기서 실패하면 tool 호출을 시도조차 하지 않는다. */
|
|
42
|
+
start(): Promise<McpResult<{
|
|
43
|
+
name: string;
|
|
44
|
+
version: string;
|
|
45
|
+
}>>;
|
|
46
|
+
/** tool 하나를 부르고 text content를 JSON으로 푼다. */
|
|
47
|
+
callTool<T>(name: string, args: Record<string, unknown>): Promise<McpResult<T>>;
|
|
48
|
+
/** 자식을 정리한다. 여러 번 불러도 안전하다. */
|
|
49
|
+
stop(): Promise<void>;
|
|
50
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// stdio MCP 최소 클라이언트.
|
|
2
|
+
//
|
|
3
|
+
// SDK를 들이지 않는다 (C-09 §8). 필요한 것은 줄 단위 JSON-RPC 왕복 하나뿐이고, 그것 때문에
|
|
4
|
+
// 의존성을 늘리면 "외부 의존이 Core로 전파되지 않는다"는 목표가 adapter 층에서부터 흔들린다.
|
|
5
|
+
//
|
|
6
|
+
// 프로토콜은 실측으로 확인했다 — Content-Length 프레이밍이 아니라 **개행 구분 JSON**이고,
|
|
7
|
+
// 서버는 stderr에 자기 로그를 쓴다.
|
|
8
|
+
//
|
|
9
|
+
// 지켜야 할 것 둘:
|
|
10
|
+
// stderr를 프로토콜로 읽지 않는다 — 진단용으로만 쓰고, 그것도 길이를 제한한다
|
|
11
|
+
// 자격 값을 절대 기록하지 않는다 — 이 파일은 토큰을 받지도, 보지도, 남기지도 않는다
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
const PROTOCOL_VERSION = '2024-11-05';
|
|
14
|
+
/** 진단으로 남길 stderr 최대 길이. 무한히 모으면 그 자체가 새는 곳이 된다. */
|
|
15
|
+
const STDERR_KEEP = 2000;
|
|
16
|
+
export class JamMcpClient {
|
|
17
|
+
#deps;
|
|
18
|
+
#child = null;
|
|
19
|
+
#pending = new Map();
|
|
20
|
+
#buffer = '';
|
|
21
|
+
#stderr = '';
|
|
22
|
+
#nextId = 1;
|
|
23
|
+
#exited = null;
|
|
24
|
+
#ready = false;
|
|
25
|
+
constructor(deps) {
|
|
26
|
+
this.#deps = deps;
|
|
27
|
+
}
|
|
28
|
+
/** 띄우고 악수까지. 여기서 실패하면 tool 호출을 시도조차 하지 않는다. */
|
|
29
|
+
async start() {
|
|
30
|
+
if (this.#ready && this.#child)
|
|
31
|
+
return { ok: true, value: { name: '', version: '' } };
|
|
32
|
+
try {
|
|
33
|
+
this.#child = this.#deps.spawnProcess
|
|
34
|
+
? this.#deps.spawnProcess()
|
|
35
|
+
: spawn(this.#deps.command, [...(this.#deps.args ?? [])], {
|
|
36
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
37
|
+
...(this.#deps.cwd ? { cwd: this.#deps.cwd } : {}),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return { ok: false, kind: 'SPAWN_FAILED', detail: String(error) };
|
|
42
|
+
}
|
|
43
|
+
const child = this.#child;
|
|
44
|
+
child.stdout.setEncoding('utf8');
|
|
45
|
+
child.stdout.on('data', (chunk) => this.#onStdout(chunk));
|
|
46
|
+
// 서버의 자기 로그다. 프로토콜로 읽지 않고, 실패했을 때 사람에게 보여줄 만큼만 남긴다.
|
|
47
|
+
child.stderr.setEncoding('utf8');
|
|
48
|
+
child.stderr.on('data', (chunk) => {
|
|
49
|
+
this.#stderr = (this.#stderr + chunk).slice(-STDERR_KEEP);
|
|
50
|
+
});
|
|
51
|
+
child.on('error', (error) => this.#fail({ kind: 'SPAWN_FAILED', detail: String(error) }));
|
|
52
|
+
// 자식이 먼저 죽으면 기다리던 요청이 영원히 매달린다. 전부 깨워서 이유를 준다.
|
|
53
|
+
child.on('exit', (code, signal) => this.#fail({
|
|
54
|
+
kind: 'PROCESS_EXITED',
|
|
55
|
+
detail: `JAM이 먼저 종료했다 (code=${code ?? 'null'}, signal=${signal ?? 'null'})${this.#tail()}`,
|
|
56
|
+
}));
|
|
57
|
+
const handshake = await this.#request('initialize', {
|
|
58
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
59
|
+
capabilities: {},
|
|
60
|
+
clientInfo: { name: 'asc', version: '0.1.0' },
|
|
61
|
+
});
|
|
62
|
+
if (!handshake.ok)
|
|
63
|
+
return handshake;
|
|
64
|
+
const info = handshake.value;
|
|
65
|
+
// 버전이 다르면 조용히 계속하지 않는다 — 뒤에서 모양이 어긋나면 원인을 못 찾는다.
|
|
66
|
+
if (info?.protocolVersion !== PROTOCOL_VERSION) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
kind: 'PROTOCOL_ERROR',
|
|
70
|
+
detail: `protocol 버전이 다르다 — 기대 ${PROTOCOL_VERSION}, 받음 ${String(info?.protocolVersion)}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
this.#notify('notifications/initialized');
|
|
74
|
+
this.#ready = true;
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
value: { name: info.serverInfo?.name ?? '', version: info.serverInfo?.version ?? '' },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** tool 하나를 부르고 text content를 JSON으로 푼다. */
|
|
81
|
+
async callTool(name, args) {
|
|
82
|
+
if (!this.#ready) {
|
|
83
|
+
const started = await this.start();
|
|
84
|
+
if (!started.ok)
|
|
85
|
+
return started;
|
|
86
|
+
}
|
|
87
|
+
const response = await this.#request('tools/call', { name, arguments: args });
|
|
88
|
+
if (!response.ok)
|
|
89
|
+
return response;
|
|
90
|
+
const payload = response.value;
|
|
91
|
+
const text = payload?.content?.find((part) => part.type === 'text')?.text;
|
|
92
|
+
if (typeof text !== 'string') {
|
|
93
|
+
return { ok: false, kind: 'PROTOCOL_ERROR', detail: `${name}: text content가 없다` };
|
|
94
|
+
}
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(text);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return { ok: false, kind: 'PROTOCOL_ERROR', detail: `${name}: 응답이 JSON이 아니다` };
|
|
101
|
+
}
|
|
102
|
+
// 서버가 실패를 알린 것과 우리가 못 읽은 것은 다르다. 앞의 것은 서버 말을 그대로 옮긴다.
|
|
103
|
+
if (payload.isError) {
|
|
104
|
+
const error = parsed?.error;
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
kind: 'TOOL_ERROR',
|
|
108
|
+
detail: error?.code ? `${error.code}: ${error.message ?? ''}` : `${name} 실패`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return { ok: true, value: parsed };
|
|
112
|
+
}
|
|
113
|
+
/** 자식을 정리한다. 여러 번 불러도 안전하다. */
|
|
114
|
+
async stop() {
|
|
115
|
+
const child = this.#child;
|
|
116
|
+
this.#child = null;
|
|
117
|
+
this.#ready = false;
|
|
118
|
+
if (!child || child.exitCode !== null)
|
|
119
|
+
return;
|
|
120
|
+
child.stdin.end();
|
|
121
|
+
child.kill();
|
|
122
|
+
}
|
|
123
|
+
// ── 내부 ──────────────────────────────────────────────────────────────────
|
|
124
|
+
#request(method, params) {
|
|
125
|
+
const child = this.#child;
|
|
126
|
+
if (!child)
|
|
127
|
+
return Promise.resolve({ ok: false, kind: 'SPAWN_FAILED', detail: '프로세스가 없다' });
|
|
128
|
+
if (this.#exited)
|
|
129
|
+
return Promise.resolve({ ok: false, ...this.#exited });
|
|
130
|
+
const id = this.#nextId++;
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
this.#pending.delete(id);
|
|
134
|
+
resolve({
|
|
135
|
+
ok: false,
|
|
136
|
+
kind: 'TIMEOUT',
|
|
137
|
+
detail: `${method}: ${this.#deps.timeoutMs ?? 30_000}ms 안에 답하지 않았다${this.#tail()}`,
|
|
138
|
+
});
|
|
139
|
+
}, this.#deps.timeoutMs ?? 30_000);
|
|
140
|
+
this.#pending.set(id, {
|
|
141
|
+
resolve: (value) => {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
resolve({ ok: true, value });
|
|
144
|
+
},
|
|
145
|
+
reject: (failure) => {
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
resolve({ ok: false, ...failure });
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
try {
|
|
151
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
this.#pending.delete(id);
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
resolve({ ok: false, kind: 'PROCESS_EXITED', detail: String(error) });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
#notify(method) {
|
|
161
|
+
try {
|
|
162
|
+
this.#child?.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method })}\n`);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// 알림은 답을 기다리지 않는다. 못 보냈으면 다음 요청이 어차피 실패로 말해 준다.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** 개행 구분 JSON. 한 줄이 깨져도 그 줄만 버리고 스트림을 통째로 포기하지 않는다. */
|
|
169
|
+
#onStdout(chunk) {
|
|
170
|
+
this.#buffer += chunk;
|
|
171
|
+
for (;;) {
|
|
172
|
+
const newline = this.#buffer.indexOf('\n');
|
|
173
|
+
if (newline < 0)
|
|
174
|
+
break;
|
|
175
|
+
const line = this.#buffer.slice(0, newline).trim();
|
|
176
|
+
this.#buffer = this.#buffer.slice(newline + 1);
|
|
177
|
+
if (line.length === 0)
|
|
178
|
+
continue;
|
|
179
|
+
let message;
|
|
180
|
+
try {
|
|
181
|
+
message = JSON.parse(line);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// 서버가 stdout에 프로토콜이 아닌 것을 흘렸다. 기다리는 요청을 매달아 두는 것보다
|
|
185
|
+
// 이유를 주고 끊는 편이 낫다.
|
|
186
|
+
this.#fail({ kind: 'PROTOCOL_ERROR', detail: '응답 한 줄이 JSON이 아니다' });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (typeof message.id !== 'number')
|
|
190
|
+
continue; // 알림이다 — 기다리는 쪽이 없다
|
|
191
|
+
const waiting = this.#pending.get(message.id);
|
|
192
|
+
if (!waiting)
|
|
193
|
+
continue; // 이미 시간이 지나 포기한 요청
|
|
194
|
+
this.#pending.delete(message.id);
|
|
195
|
+
if (message.error) {
|
|
196
|
+
waiting.reject({
|
|
197
|
+
kind: 'PROTOCOL_ERROR',
|
|
198
|
+
detail: `JSON-RPC 오류 ${message.error.code ?? ''}: ${message.error.message ?? ''}`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
waiting.resolve(message.result);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** 기다리는 요청 전부를 같은 이유로 깨운다. */
|
|
207
|
+
#fail(failure) {
|
|
208
|
+
this.#exited = failure;
|
|
209
|
+
for (const waiting of this.#pending.values())
|
|
210
|
+
waiting.reject(failure);
|
|
211
|
+
this.#pending.clear();
|
|
212
|
+
}
|
|
213
|
+
/** 진단 꼬리표. 서버 로그의 마지막 조각만 붙인다 — 자격 값은 애초에 여기 오지 않는다. */
|
|
214
|
+
#tail() {
|
|
215
|
+
const tail = this.#stderr.trim().split('\n').at(-1);
|
|
216
|
+
return tail ? ` — 서버 마지막 출력: ${tail.slice(0, 200)}` : '';
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { InventoryPage, InventoryPort, InventoryQuery } from '../../ports/inventory.ts';
|
|
2
|
+
import type { CommentQuery, ContextComment, ResourceContextPort, ResourceSnapshot } from '../../ports/resource-context.ts';
|
|
3
|
+
import type { JamMcpClient } from './mcp-client.ts';
|
|
4
|
+
/** JAM이 모든 결과에 붙여 주는 완결성 표식. 이 값을 접으면 잘린 목록이 "전체"가 된다. */
|
|
5
|
+
export type JamMeta = {
|
|
6
|
+
complete?: boolean;
|
|
7
|
+
commentsComplete?: boolean;
|
|
8
|
+
overflow?: string[];
|
|
9
|
+
missingKeys?: string[];
|
|
10
|
+
reason?: string;
|
|
11
|
+
notes?: string[];
|
|
12
|
+
};
|
|
13
|
+
export type JamPortsDeps = {
|
|
14
|
+
client: JamMcpClient;
|
|
15
|
+
/** Jira project key. `.jira-agent/project.yaml` 이 선언한 값이며 추측하지 않는다. */
|
|
16
|
+
projectKey: string;
|
|
17
|
+
/**
|
|
18
|
+
* JQL 날짜 리터럴을 해석할 timezone(IANA). **Jira 계정의 timezone이며 기계의 것이
|
|
19
|
+
* 아니다.** 선언하지 않으면 `DEFAULT_TIMEZONE`.
|
|
20
|
+
*/
|
|
21
|
+
timezone?: string;
|
|
22
|
+
};
|
|
23
|
+
declare class JamBase {
|
|
24
|
+
readonly id = "jam";
|
|
25
|
+
protected client: JamMcpClient;
|
|
26
|
+
protected projectKey: string;
|
|
27
|
+
protected timezone: string;
|
|
28
|
+
constructor(deps: JamPortsDeps);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 상태 무관 열거. 여기서 `scope` 를 고르는 규칙이 이 adapter의 핵심 판단이다.
|
|
32
|
+
*
|
|
33
|
+
* 이 Port의 소비자는 회수 경로(Reconcile·Census)이고 그 둘은 **빠짐없음이 성립해야**
|
|
34
|
+
* 의미가 있다. 그래서 항상 `complete` 로 부른다 — "호출이 작아 보이니 preview" 같은
|
|
35
|
+
* 추측으로 정하지 않는다. 값싼 훑기(preview)는 쓸 자리가 생기면 그때 연다.
|
|
36
|
+
*/
|
|
37
|
+
export declare class JamInventory extends JamBase implements InventoryPort {
|
|
38
|
+
enumerate(query: InventoryQuery, cursor?: string): Promise<InventoryPage>;
|
|
39
|
+
}
|
|
40
|
+
export declare class JamResourceContext extends JamBase implements ResourceContextPort {
|
|
41
|
+
getResource(reference: string): Promise<ResourceSnapshot>;
|
|
42
|
+
/**
|
|
43
|
+
* 논의는 full에만 실린다.
|
|
44
|
+
*
|
|
45
|
+
* **`commentsComplete === false` 를 "댓글 없음"으로 접지 않는다.** 예산을 넘기면 JAM이
|
|
46
|
+
* 오래된 것부터 떨어뜨리는데, 그것을 조용히 받으면 조사 단계가 "논의를 다 봤다"고
|
|
47
|
+
* 착각한다. 그래서 잘렸다는 사실을 항목 하나로 끼워 넣어 사람이 보게 한다.
|
|
48
|
+
*/
|
|
49
|
+
getComments(reference: string, query?: CommentQuery): Promise<ContextComment[]>;
|
|
50
|
+
}
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// JAM 작업 항목 Adapter — Inventory · ResourceContext.
|
|
2
|
+
//
|
|
3
|
+
// ASC는 이 파일 밖에서 Jira를 알지 못한다. REST·자격·문서 포맷·pagination 정책은 전부
|
|
4
|
+
// JAM 뒤에 있고, 여기서는 tool 세 개의 응답을 Port 모양으로 옮기기만 한다.
|
|
5
|
+
//
|
|
6
|
+
// 이 adapter가 **제공하지 않는 것**이 제공하는 것만큼 중요하다:
|
|
7
|
+
// context.history JAM이 changelog를 주지 않는다. 없는 것을 있는 척하지 않는다
|
|
8
|
+
// observe.delta 푸시가 없다. `updated >= …` 조회는 EventSource가 아니라 회수 경로다
|
|
9
|
+
/**
|
|
10
|
+
* `updated` 는 분 단위 정밀도라 기준선을 그대로 쓰면 같은 분에 몰린 변경을 놓친다.
|
|
11
|
+
* 조금 뒤로 물려 겹쳐 읽는다 — 누락보다 중복이 안전하고, 중복은 revision 비교가 거른다
|
|
12
|
+
* (OM §10.5, event-source의 overlap과 같은 이유).
|
|
13
|
+
*/
|
|
14
|
+
const OVERLAP_MS = 60_000;
|
|
15
|
+
/**
|
|
16
|
+
* JQL이 시각을 읽는 timezone. **JQL 날짜 리터럴에는 offset을 적을 자리가 없다** —
|
|
17
|
+
* `"2026/08/26 09:04"` 는 Jira 계정의 timezone으로 해석된다. 그래서 이 값은 ASC가 도는
|
|
18
|
+
* 기계의 timezone이 아니라 **Jira 쪽 timezone**이며, 선언하지 않으면 UTC로 읽는다.
|
|
19
|
+
*
|
|
20
|
+
* 기본값을 host timezone으로 두지 않는 이유: 같은 순간이 기계마다 다른 JQL이 되고,
|
|
21
|
+
* 그 차이는 조회 결과의 차이라 조용히 누락으로 나타난다(3-OS CI에서 실제로 드러났다).
|
|
22
|
+
*
|
|
23
|
+
* 잘못 선언했을 때의 방향도 남긴다 — 실제 Jira보다 **동쪽**을 선언하면 기준선이 앞으로
|
|
24
|
+
* 밀려 그만큼을 놓치고, **서쪽**을 선언하면 겹쳐 읽어 중복이 는다. 모를 때의 UTC는
|
|
25
|
+
* 한국·일본처럼 동쪽 계정에 대해 안전한 쪽(중복)으로 틀린다.
|
|
26
|
+
*/
|
|
27
|
+
const DEFAULT_TIMEZONE = 'UTC';
|
|
28
|
+
/**
|
|
29
|
+
* JQL 리터럴로 안전한 시각 문자열. JAM은 JQL을 그대로 통과시키므로 여기서 다듬는다.
|
|
30
|
+
*
|
|
31
|
+
* 같은 순간은 어느 기계에서 불러도 같은 문자열이어야 한다 — 그래서 `Date` 의 host 기준
|
|
32
|
+
* getter(`getHours()` 등)를 쓰지 않고 timezone을 명시해 서식한다.
|
|
33
|
+
*/
|
|
34
|
+
function jqlTime(iso, timeZone) {
|
|
35
|
+
const at = new Date(new Date(iso).getTime() - OVERLAP_MS);
|
|
36
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
37
|
+
timeZone,
|
|
38
|
+
hour12: false,
|
|
39
|
+
year: 'numeric',
|
|
40
|
+
month: '2-digit',
|
|
41
|
+
day: '2-digit',
|
|
42
|
+
hour: '2-digit',
|
|
43
|
+
minute: '2-digit',
|
|
44
|
+
}).formatToParts(at);
|
|
45
|
+
const of = (type) => parts.find((p) => p.type === type)?.value ?? '';
|
|
46
|
+
// `hour12: false` 로도 자정을 `24` 로 주는 구현이 있다 — 그대로 두면 JQL이 거절한다.
|
|
47
|
+
const hour = of('hour') === '24' ? '00' : of('hour');
|
|
48
|
+
return `${of('year')}/${of('month')}/${of('day')} ${hour}:${of('minute')}`;
|
|
49
|
+
}
|
|
50
|
+
class JamBase {
|
|
51
|
+
id = 'jam';
|
|
52
|
+
client;
|
|
53
|
+
projectKey;
|
|
54
|
+
timezone;
|
|
55
|
+
constructor(deps) {
|
|
56
|
+
this.client = deps.client;
|
|
57
|
+
this.projectKey = deps.projectKey;
|
|
58
|
+
this.timezone = deps.timezone ?? DEFAULT_TIMEZONE;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 상태 무관 열거. 여기서 `scope` 를 고르는 규칙이 이 adapter의 핵심 판단이다.
|
|
63
|
+
*
|
|
64
|
+
* 이 Port의 소비자는 회수 경로(Reconcile·Census)이고 그 둘은 **빠짐없음이 성립해야**
|
|
65
|
+
* 의미가 있다. 그래서 항상 `complete` 로 부른다 — "호출이 작아 보이니 preview" 같은
|
|
66
|
+
* 추측으로 정하지 않는다. 값싼 훑기(preview)는 쓸 자리가 생기면 그때 연다.
|
|
67
|
+
*/
|
|
68
|
+
export class JamInventory extends JamBase {
|
|
69
|
+
async enumerate(query, cursor) {
|
|
70
|
+
// JAM은 cursor를 노출하지 않는다. 페이지 순회는 JAM 안에서 끝나므로 ASC가 이어받을
|
|
71
|
+
// 지점이 없다 — 두 번째 페이지 요청은 성립하지 않는다.
|
|
72
|
+
if (cursor)
|
|
73
|
+
return { items: [], complete: true };
|
|
74
|
+
const clauses = [`project = ${this.projectKey}`];
|
|
75
|
+
if (query.updatedSince)
|
|
76
|
+
clauses.push(`updated >= "${jqlTime(query.updatedSince, this.timezone)}"`);
|
|
77
|
+
const response = await this.client.callTool('jira_search', {
|
|
78
|
+
jql: `${clauses.join(' AND ')} ORDER BY updated ASC`,
|
|
79
|
+
scope: 'complete',
|
|
80
|
+
});
|
|
81
|
+
// 못 읽은 것을 "없다"로 돌려주면 census가 멀쩡한 항목을 사라졌다고 본다.
|
|
82
|
+
if (!response.ok)
|
|
83
|
+
return { items: [], complete: false };
|
|
84
|
+
const { issues = [], meta = {} } = response.value;
|
|
85
|
+
return {
|
|
86
|
+
items: issues.map((issue) => ({
|
|
87
|
+
reference: issue.key,
|
|
88
|
+
state: issue.status,
|
|
89
|
+
updatedAt: issue.updated,
|
|
90
|
+
// 이 provider가 주는 유일한 변화 신호다. 필드 단위 delta는 알 수 없다.
|
|
91
|
+
revisionMarker: issue.updated,
|
|
92
|
+
title: issue.summary,
|
|
93
|
+
assignees: issue.assignee ? [issue.assignee] : [],
|
|
94
|
+
labels: [...(issue.labels ?? []), ...(issue.components ?? [])],
|
|
95
|
+
})),
|
|
96
|
+
// JAM이 완결을 부인하면 그대로 옮긴다. Core가 이 값을 보고 상실 판정·기준선 이동을
|
|
97
|
+
// 모두 보류한다 (C-07 §1.5·§8.2).
|
|
98
|
+
complete: meta.complete === true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export class JamResourceContext extends JamBase {
|
|
103
|
+
async getResource(reference) {
|
|
104
|
+
const response = await this.client.callTool('jira_context', { issueKeys: [reference] });
|
|
105
|
+
if (!response.ok)
|
|
106
|
+
return missing(reference);
|
|
107
|
+
const issue = response.value.issues?.[0];
|
|
108
|
+
// 안 돌아온 키는 없는 것일 수도, 안 보이는 것일 수도 있다. provider가 둘을 합쳐 주므로
|
|
109
|
+
// 우리도 합쳐진 사실 이상을 지어내지 않는다.
|
|
110
|
+
if (!issue)
|
|
111
|
+
return missing(reference);
|
|
112
|
+
const related = [
|
|
113
|
+
...(issue.parent?.key ? [issue.parent.key] : []),
|
|
114
|
+
...(issue.subtasks ?? []).flatMap((sub) => (sub.key ? [sub.key] : [])),
|
|
115
|
+
...(issue.links ?? []).flatMap((link) => (link.issue?.key ? [link.issue.key] : [])),
|
|
116
|
+
];
|
|
117
|
+
return {
|
|
118
|
+
reference,
|
|
119
|
+
state: issue.status,
|
|
120
|
+
title: issue.summary,
|
|
121
|
+
...(issue.description ? { body: issue.description } : {}),
|
|
122
|
+
...(issue.assignee ? { assignees: [issue.assignee] } : {}),
|
|
123
|
+
labels: [...(issue.labels ?? []), ...(issue.components ?? [])],
|
|
124
|
+
updatedAt: issue.updated,
|
|
125
|
+
revisionMarker: issue.updated,
|
|
126
|
+
...(related.length > 0 ? { related } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 논의는 full에만 실린다.
|
|
131
|
+
*
|
|
132
|
+
* **`commentsComplete === false` 를 "댓글 없음"으로 접지 않는다.** 예산을 넘기면 JAM이
|
|
133
|
+
* 오래된 것부터 떨어뜨리는데, 그것을 조용히 받으면 조사 단계가 "논의를 다 봤다"고
|
|
134
|
+
* 착각한다. 그래서 잘렸다는 사실을 항목 하나로 끼워 넣어 사람이 보게 한다.
|
|
135
|
+
*/
|
|
136
|
+
async getComments(reference, query = {}) {
|
|
137
|
+
const response = await this.client.callTool('jira_full', { issueKeys: [reference] });
|
|
138
|
+
if (!response.ok)
|
|
139
|
+
return [];
|
|
140
|
+
const issue = response.value.issues?.[0];
|
|
141
|
+
if (!issue)
|
|
142
|
+
return [];
|
|
143
|
+
const comments = (issue.comments ?? []).slice(0, query.limit ?? 20).map((comment) => ({
|
|
144
|
+
id: comment.id,
|
|
145
|
+
author: comment.author ?? '(unknown)',
|
|
146
|
+
at: comment.updated ?? comment.created,
|
|
147
|
+
body: comment.body,
|
|
148
|
+
}));
|
|
149
|
+
if (response.value.meta?.commentsComplete === false) {
|
|
150
|
+
comments.push({
|
|
151
|
+
id: 'jam:incomplete',
|
|
152
|
+
author: '(ASC)',
|
|
153
|
+
at: new Date(0).toISOString(),
|
|
154
|
+
body: '이 논의는 일부만 받았다 — provider가 예산 안에서 잘라 보냈다. 전부 보려면 원본을 열어라.',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return comments;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const missing = (reference) => ({
|
|
161
|
+
reference,
|
|
162
|
+
state: 'unknown',
|
|
163
|
+
title: '',
|
|
164
|
+
updatedAt: '',
|
|
165
|
+
revisionMarker: '',
|
|
166
|
+
missing: true,
|
|
167
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IdentityBinding } from '../../ports/approval.ts';
|
|
2
|
+
export type IdentityMap = Readonly<Record<string, readonly string[]>>;
|
|
3
|
+
/**
|
|
4
|
+
* `controller identity → [채널:actor, ...]` 매핑으로 판정한다.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* new LocalIdentityBinding({ 'controller-a': ['local:colosair', 'mattermost:@colosair'] })
|
|
8
|
+
*/
|
|
9
|
+
export declare class LocalIdentityBinding implements IdentityBinding {
|
|
10
|
+
#private;
|
|
11
|
+
constructor(map: IdentityMap);
|
|
12
|
+
verify({ channel, actor, authorizedApprover, }: {
|
|
13
|
+
channel: string;
|
|
14
|
+
actor: string;
|
|
15
|
+
authorizedApprover: string;
|
|
16
|
+
}): Promise<boolean>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* **테스트와 명시적 opt-in 전용. 정상 경로에서 쓰지 말 것.**
|
|
20
|
+
* 켜는 순간 승인자 검증이 사라져 어떤 이름으로 부르든 통과한다.
|
|
21
|
+
* 실제 운용에서 이걸 고르는 것은 승인 계층을 없애는 결정이며, 그렇게 고른 코드는
|
|
22
|
+
* 그 사실을 사람에게 보여야 한다.
|
|
23
|
+
*/
|
|
24
|
+
export declare class UnverifiedIdentityBinding implements IdentityBinding {
|
|
25
|
+
readonly unverified = true;
|
|
26
|
+
verify(): Promise<boolean>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Local Identity Binding — 어떤 actor가 승인 권한자인지 판정한다 (OM §11.6).
|
|
2
|
+
//
|
|
3
|
+
// **이 계층이 하는 일과 하지 않는 일**
|
|
4
|
+
// 한다: 주어진 `채널:actor` 가 그 요청의 승인 권한자로 지정돼 있는지 확인한다.
|
|
5
|
+
// 로컬이라는 이유만으로 통과시키지 않고, 매핑에 없는 이름은 거절한다.
|
|
6
|
+
// 안 한다: 지금 명령을 낸 주체가 사람이라는 증명. 셸을 쓸 수 있는 Agent는 매핑된
|
|
7
|
+
// 이름을 그대로 댈 수 있다. 이 매핑은 신원 확인이지 현장 인증이 아니다.
|
|
8
|
+
//
|
|
9
|
+
// 강한 local authentication(OS 사용자 확인·서명·대화형 확인)은 별도 후속 범위다.
|
|
10
|
+
// 그전까지 결정 표면은 "사람이 조작하는 표면"이라는 계약 위에서 동작한다.
|
|
11
|
+
//
|
|
12
|
+
// Identity는 Credential이 아니다. 여기에는 누가 누구인지만 있고 비밀은 없다 (OM §4.5).
|
|
13
|
+
// Profile/Override에서 매핑을 읽어오는 경로는 B-10에서 붙인다.
|
|
14
|
+
/**
|
|
15
|
+
* `controller identity → [채널:actor, ...]` 매핑으로 판정한다.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* new LocalIdentityBinding({ 'controller-a': ['local:colosair', 'mattermost:@colosair'] })
|
|
19
|
+
*/
|
|
20
|
+
export class LocalIdentityBinding {
|
|
21
|
+
#allowed;
|
|
22
|
+
constructor(map) {
|
|
23
|
+
this.#allowed = new Map(Object.entries(map).map(([approver, ids]) => [approver, new Set(ids)]));
|
|
24
|
+
}
|
|
25
|
+
async verify({ channel, actor, authorizedApprover, }) {
|
|
26
|
+
return this.#allowed.get(authorizedApprover)?.has(`${channel}:${actor}`) ?? false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* **테스트와 명시적 opt-in 전용. 정상 경로에서 쓰지 말 것.**
|
|
31
|
+
* 켜는 순간 승인자 검증이 사라져 어떤 이름으로 부르든 통과한다.
|
|
32
|
+
* 실제 운용에서 이걸 고르는 것은 승인 계층을 없애는 결정이며, 그렇게 고른 코드는
|
|
33
|
+
* 그 사실을 사람에게 보여야 한다.
|
|
34
|
+
*/
|
|
35
|
+
export class UnverifiedIdentityBinding {
|
|
36
|
+
unverified = true;
|
|
37
|
+
async verify() {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DecisionSummary } from '../../core/view/decision-view.ts';
|
|
2
|
+
import type { DeliveryOutcome, DigestBatch, PresentationCapability, PresentationPort } from '../../ports/presentation.ts';
|
|
3
|
+
export type LocalPresentationDeps = {
|
|
4
|
+
/** 어디로 내보낼지. 주입받는 이유는 테스트가 화면을 붙잡지 않기 위해서다. */
|
|
5
|
+
write?: (line: string) => void;
|
|
6
|
+
};
|
|
7
|
+
export declare class LocalPresentation implements PresentationPort {
|
|
8
|
+
#private;
|
|
9
|
+
readonly id = "local";
|
|
10
|
+
readonly capabilities: ReadonlySet<PresentationCapability>;
|
|
11
|
+
constructor(deps?: LocalPresentationDeps);
|
|
12
|
+
presentDigest(batch: DigestBatch): Promise<DeliveryOutcome>;
|
|
13
|
+
presentUrgent(item: DecisionSummary): Promise<DeliveryOutcome>;
|
|
14
|
+
}
|