@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,7 @@
|
|
|
1
|
+
import type { Rendered, RenderDensity, Renderer } from '../../ports/renderer.ts';
|
|
2
|
+
import type { DecisionSummary, DecisionView } from '../../core/view/decision-view.ts';
|
|
3
|
+
export declare class TextRenderer implements Renderer {
|
|
4
|
+
readonly id = "text";
|
|
5
|
+
renderDecision(view: DecisionView, density: RenderDensity): Rendered;
|
|
6
|
+
renderList(items: readonly DecisionSummary[]): Rendered;
|
|
7
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// 텍스트 Renderer — 터미널과 로그에 쓰는 기본 표현.
|
|
2
|
+
//
|
|
3
|
+
// 지키는 것 둘 (C-01 §2·§6):
|
|
4
|
+
// 1. reference는 어떤 밀도에서도 첫 줄에 남는다. 다른 채널에서 같은 요청을 지목하는
|
|
5
|
+
// 유일한 수단이라 접기의 대상이 아니다.
|
|
6
|
+
// 2. 알림 당시 분석과 지금 다시 읽은 사실은 다른 제목 아래 놓는다. 한 문단에 섞으면
|
|
7
|
+
// 읽는 사람이 과거 판단을 현재 사실로 받아들인다.
|
|
8
|
+
/** 조회 시점에 무엇이 달라졌는지 한 줄로 알려준다 — 사용자가 먼저 읽어야 할 경고다. */
|
|
9
|
+
const FRESHNESS_NOTE = {
|
|
10
|
+
CURRENT: '알림 이후 달라진 것 없음',
|
|
11
|
+
STALE_CONTEXT: '주의 — 알림 이후 작업 맥락이 바뀌었다',
|
|
12
|
+
SOURCE_CHANGED: '주의 — 알림 이후 원본(스레드·정본)이 바뀌었다',
|
|
13
|
+
ALREADY_DECIDED: '이미 결정된 요청이다',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* 원본을 확인하지 못한 채 얻은 "변화 없음"을 확인하고 얻은 것처럼 보이게 두지 않는다.
|
|
17
|
+
* 무엇을 근거로 한 판단인지 밝히는 것이 판단 자체보다 중요할 때가 있다.
|
|
18
|
+
*/
|
|
19
|
+
function freshnessLine(view) {
|
|
20
|
+
const unverifiedSource = view.verification.source === 'UNAVAILABLE';
|
|
21
|
+
const scope = unverifiedSource && view.freshness === 'CURRENT' ? ' (로컬 기준)' : '';
|
|
22
|
+
const lines = [`상태: ${view.stored.status} (v${view.version}) · ${FRESHNESS_NOTE[view.freshness]}${scope}`];
|
|
23
|
+
if (unverifiedSource)
|
|
24
|
+
lines.push('원본 변경 여부 미확인 — 외부 연결 없음');
|
|
25
|
+
if (view.verification.localContext === 'NOT_APPLICABLE') {
|
|
26
|
+
lines.push('현재 작업과의 관계 판단 불가 — 요청이 영향 세션을 지목하지 않았다');
|
|
27
|
+
}
|
|
28
|
+
return lines;
|
|
29
|
+
}
|
|
30
|
+
export class TextRenderer {
|
|
31
|
+
id = 'text';
|
|
32
|
+
renderDecision(view, density) {
|
|
33
|
+
const lines = [view.reference, view.stored.title, ''];
|
|
34
|
+
lines.push(...freshnessLine(view));
|
|
35
|
+
if (view.decided) {
|
|
36
|
+
lines.push(`결정: ${view.decided.kind} — ${view.decided.actor} (${view.decided.channel}) ${view.decided.decidedAt}`);
|
|
37
|
+
if (view.resultRef)
|
|
38
|
+
lines.push(`결과: ${view.resultRef}`);
|
|
39
|
+
}
|
|
40
|
+
if (density === 'summary') {
|
|
41
|
+
lines.push('', `선택: ${view.allowedDecisions.join(' / ')}`);
|
|
42
|
+
return { density, text: lines.join('\n') };
|
|
43
|
+
}
|
|
44
|
+
lines.push('', '[알림 당시 분석]', `감지: ${view.stored.detectedAt} · ${view.stored.source}`, view.stored.situation);
|
|
45
|
+
if (view.stored.context)
|
|
46
|
+
lines.push('', `맥락: ${view.stored.context}`);
|
|
47
|
+
lines.push('', `작업 중단 필요: ${view.stored.interruptRequired ? 'Yes' : 'No'}`, `기준 세션: ${view.stored.affectedSessions.join(', ') || '없음'}`);
|
|
48
|
+
if (view.stored.rationale)
|
|
49
|
+
lines.push(`판단 근거: ${view.stored.rationale}`);
|
|
50
|
+
if (view.stored.snapshot.length > 0) {
|
|
51
|
+
lines.push('스냅샷: ' + view.stored.snapshot.map((s) => `${s.sourceId} @ ${s.baseline}`).join(' · '));
|
|
52
|
+
}
|
|
53
|
+
if (view.stored.recommendation)
|
|
54
|
+
lines.push('', `권장 대응: ${view.stored.recommendation}`);
|
|
55
|
+
if (view.stored.draft)
|
|
56
|
+
lines.push('', '[답변 초안]', view.stored.draft);
|
|
57
|
+
if (view.current) {
|
|
58
|
+
lines.push('', '[현재 작업 기준]', `관측: ${view.current.observedAt}`, `활성 세션: ${view.current.activeSessions.join(', ') || '없음'}`, `영향: ${view.current.affectsCurrentWork ? '있음' : '없음'}`);
|
|
59
|
+
for (const change of view.current.canonicalChanges) {
|
|
60
|
+
lines.push(`정본 변경: ${change.sourceId} ${change.before ?? '(기록 없음)'} → ${change.after}`);
|
|
61
|
+
}
|
|
62
|
+
for (const note of view.current.notes)
|
|
63
|
+
lines.push(`· ${note}`);
|
|
64
|
+
}
|
|
65
|
+
lines.push('', `승인 권한자: ${view.authorizedApprover}`, `선택: ${view.allowedDecisions.join(' / ')}`);
|
|
66
|
+
if (view.expiresAt)
|
|
67
|
+
lines.push(`만료: ${view.expiresAt}`);
|
|
68
|
+
return { density, text: lines.join('\n') };
|
|
69
|
+
}
|
|
70
|
+
renderList(items) {
|
|
71
|
+
if (items.length === 0)
|
|
72
|
+
return { density: 'summary', text: '대기 중인 요청 없음' };
|
|
73
|
+
const text = items
|
|
74
|
+
.map((i) => {
|
|
75
|
+
const stale = i.freshness === 'CURRENT' ? '' : ` [${i.freshness}]`;
|
|
76
|
+
return `${i.requestId} ${i.priority} ${i.status} ${i.title}${stale}\n ${i.reference}`;
|
|
77
|
+
})
|
|
78
|
+
.join('\n');
|
|
79
|
+
return { density: 'summary', text };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ScopedStore } from '../../ports/state-store.ts';
|
|
3
|
+
import type { Cursor, EventBatch, EventSource, RawEvent } from '../../ports/event-source.ts';
|
|
4
|
+
export declare const IngressRecord: z.ZodObject<{
|
|
5
|
+
/** 수신 순번. drain은 이 순서로 돌려준다 — 도착 순서가 곧 관측 순서다. */
|
|
6
|
+
seq: z.ZodNumber;
|
|
7
|
+
receivedAt: z.ZodString;
|
|
8
|
+
event: z.ZodObject<{
|
|
9
|
+
eventKey: z.ZodString;
|
|
10
|
+
detectedAt: z.ZodString;
|
|
11
|
+
reference: z.ZodString;
|
|
12
|
+
hints: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
13
|
+
raw: z.ZodOptional<z.ZodUnknown>;
|
|
14
|
+
}, "strip", z.ZodTypeAny, {
|
|
15
|
+
detectedAt: string;
|
|
16
|
+
eventKey: string;
|
|
17
|
+
reference: string;
|
|
18
|
+
raw?: unknown;
|
|
19
|
+
hints?: Record<string, unknown> | undefined;
|
|
20
|
+
}, {
|
|
21
|
+
detectedAt: string;
|
|
22
|
+
eventKey: string;
|
|
23
|
+
reference: string;
|
|
24
|
+
raw?: unknown;
|
|
25
|
+
hints?: Record<string, unknown> | undefined;
|
|
26
|
+
}>;
|
|
27
|
+
}, "strip", z.ZodTypeAny, {
|
|
28
|
+
event: {
|
|
29
|
+
detectedAt: string;
|
|
30
|
+
eventKey: string;
|
|
31
|
+
reference: string;
|
|
32
|
+
raw?: unknown;
|
|
33
|
+
hints?: Record<string, unknown> | undefined;
|
|
34
|
+
};
|
|
35
|
+
seq: number;
|
|
36
|
+
receivedAt: string;
|
|
37
|
+
}, {
|
|
38
|
+
event: {
|
|
39
|
+
detectedAt: string;
|
|
40
|
+
eventKey: string;
|
|
41
|
+
reference: string;
|
|
42
|
+
raw?: unknown;
|
|
43
|
+
hints?: Record<string, unknown> | undefined;
|
|
44
|
+
};
|
|
45
|
+
seq: number;
|
|
46
|
+
receivedAt: string;
|
|
47
|
+
}>;
|
|
48
|
+
export type IngressRecord = z.infer<typeof IngressRecord>;
|
|
49
|
+
export type VerifyOutcome = {
|
|
50
|
+
ok: true;
|
|
51
|
+
} | {
|
|
52
|
+
ok: false;
|
|
53
|
+
reason: 'NO_SIGNATURE' | 'BAD_SIGNATURE';
|
|
54
|
+
detail: string;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* HMAC 서명 확인 (C-12 불변식 ⑧).
|
|
58
|
+
*
|
|
59
|
+
* 길이가 달라도 같은 시간을 쓰도록 비교 전에 맞춘다 — 길이로 새는 정보도 정보다.
|
|
60
|
+
*/
|
|
61
|
+
export declare function verifySignature(payload: string, signature: string | undefined, secret: string): VerifyOutcome;
|
|
62
|
+
export type AcceptOutcome = {
|
|
63
|
+
ok: true;
|
|
64
|
+
seq: number;
|
|
65
|
+
} | {
|
|
66
|
+
ok: false;
|
|
67
|
+
reason: 'NO_SIGNATURE' | 'BAD_SIGNATURE' | 'UNREADABLE';
|
|
68
|
+
detail: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* 받은 것을 쌓는다. **여기서 판정하지 않는다** — 분류·관련성·억제는 전부 기존 Phase A가 한다.
|
|
72
|
+
*
|
|
73
|
+
* 저장은 순번마다 다른 키에 setIfAbsent다. 읽고-고쳐-쓰면 동시에 도착한 둘 중 하나가
|
|
74
|
+
* 조용히 사라지는데, 그건 "변경 없음"으로 보인다.
|
|
75
|
+
*/
|
|
76
|
+
export declare class WebhookIngress {
|
|
77
|
+
#private;
|
|
78
|
+
constructor(scope: ScopedStore, secret: string, now?: () => string);
|
|
79
|
+
accept(payload: string, signature: string | undefined, toEvent: (parsed: unknown) => RawEvent | null): Promise<AcceptOutcome>;
|
|
80
|
+
/** 쌓인 것을 순서대로 꺼내고 지운다. 꺼낸 것은 Engine이 지고, 여기 남기지 않는다. */
|
|
81
|
+
drain(limit?: number): Promise<RawEvent[]>;
|
|
82
|
+
pending(): Promise<number>;
|
|
83
|
+
/** 튕긴 수. 0이 아니면 무언가 잘못 설정돼 있다는 뜻이고, 그건 감지 공백이다. */
|
|
84
|
+
rejected(): Promise<Record<string, number>>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 버퍼를 비우는 EventSource. **Engine은 이것이 push인지 pull인지 모른다** —
|
|
88
|
+
* `ports/event-source.ts` 가 예고한 바로 그 형태다.
|
|
89
|
+
*
|
|
90
|
+
* cursor를 쓰지 않는다: 꺼낸 것은 지워지므로 다음 회차는 남은 것부터 본다. 중복은
|
|
91
|
+
* 기존 dedupe(eventKey exact lookup)가 거른다 — 같은 변경을 회수 경로가 다시 봐도
|
|
92
|
+
* 패킷이 둘이 되지 않는 이유와 같다.
|
|
93
|
+
*/
|
|
94
|
+
export declare class IngressEventSource implements EventSource {
|
|
95
|
+
#private;
|
|
96
|
+
readonly id: string;
|
|
97
|
+
constructor(deps: {
|
|
98
|
+
id?: string;
|
|
99
|
+
ingress: WebhookIngress;
|
|
100
|
+
limit?: number;
|
|
101
|
+
});
|
|
102
|
+
drain(_cursor: Cursor): Promise<EventBatch>;
|
|
103
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Webhook Ingress — 밀려 들어오는 사건을 받아 두는 자리 (C-12 §2).
|
|
2
|
+
//
|
|
3
|
+
// Webhook은 새 개념이 아니다. C-07 §1.1이 Delta를 "webhook / incremental polling /
|
|
4
|
+
// provider notification / updated-since" 로 이미 정의했고, `ports/event-source.ts` 는
|
|
5
|
+
// **push형 Adapter는 수신분을 버퍼에 쌓아 두고 drain에서 돌려준다** 고 예고해 뒀다.
|
|
6
|
+
// 이 파일은 그 예고를 실제로 구현할 뿐이며 MonitorEngine은 한 줄도 바뀌지 않는다.
|
|
7
|
+
//
|
|
8
|
+
// 책임을 둘로 나눈다 (C-12 불변식 ⑦):
|
|
9
|
+
//
|
|
10
|
+
// Ingress 받는다 · 검증한다 · 쌓는다 ← 이 파일
|
|
11
|
+
// Orchestrator 언제 처리할지 정한다 ← core/runtime/orchestrator.ts
|
|
12
|
+
//
|
|
13
|
+
// 지금은 같은 프로세스에 둘 수 있지만 Core는 그 배치를 모른다. 나중에 receiver만
|
|
14
|
+
// 떼어내도 계약은 그대로다.
|
|
15
|
+
//
|
|
16
|
+
// **서명 검증은 생략할 수 없다** (불변식 ⑧). 검증 실패는 조용한 무시가 아니라 거부이며,
|
|
17
|
+
// 거부한 사실도 기록에 남는다 — 아무도 안 오는 것과 오는데 다 튕기는 것은 다른 상태다.
|
|
18
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
export const IngressRecord = z.object({
|
|
21
|
+
/** 수신 순번. drain은 이 순서로 돌려준다 — 도착 순서가 곧 관측 순서다. */
|
|
22
|
+
seq: z.number().int().positive(),
|
|
23
|
+
receivedAt: z.string().min(1),
|
|
24
|
+
event: z.object({
|
|
25
|
+
eventKey: z.string().min(1),
|
|
26
|
+
detectedAt: z.string().min(1),
|
|
27
|
+
reference: z.string().min(1),
|
|
28
|
+
hints: z.record(z.unknown()).optional(),
|
|
29
|
+
raw: z.unknown().optional(),
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
const bufferKey = (seq) => `ingress:${String(seq).padStart(12, '0')}`;
|
|
33
|
+
const BUFFER_PREFIX = 'ingress:';
|
|
34
|
+
const REJECTED_KEY = 'ingress-rejected';
|
|
35
|
+
/**
|
|
36
|
+
* HMAC 서명 확인 (C-12 불변식 ⑧).
|
|
37
|
+
*
|
|
38
|
+
* 길이가 달라도 같은 시간을 쓰도록 비교 전에 맞춘다 — 길이로 새는 정보도 정보다.
|
|
39
|
+
*/
|
|
40
|
+
export function verifySignature(payload, signature, secret) {
|
|
41
|
+
if (!signature)
|
|
42
|
+
return { ok: false, reason: 'NO_SIGNATURE', detail: '서명 헤더가 없다' };
|
|
43
|
+
const expected = createHmac('sha256', secret).update(payload).digest('hex');
|
|
44
|
+
// provider마다 `sha256=` 같은 접두어를 붙인다. 값만 본다.
|
|
45
|
+
const given = signature.includes('=') ? signature.slice(signature.indexOf('=') + 1) : signature;
|
|
46
|
+
const a = Buffer.from(expected, 'utf8');
|
|
47
|
+
const b = Buffer.from(given, 'utf8');
|
|
48
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
49
|
+
return { ok: false, reason: 'BAD_SIGNATURE', detail: '서명이 맞지 않는다' };
|
|
50
|
+
}
|
|
51
|
+
return { ok: true };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 받은 것을 쌓는다. **여기서 판정하지 않는다** — 분류·관련성·억제는 전부 기존 Phase A가 한다.
|
|
55
|
+
*
|
|
56
|
+
* 저장은 순번마다 다른 키에 setIfAbsent다. 읽고-고쳐-쓰면 동시에 도착한 둘 중 하나가
|
|
57
|
+
* 조용히 사라지는데, 그건 "변경 없음"으로 보인다.
|
|
58
|
+
*/
|
|
59
|
+
export class WebhookIngress {
|
|
60
|
+
#scope;
|
|
61
|
+
#secret;
|
|
62
|
+
#now;
|
|
63
|
+
constructor(scope, secret, now = () => new Date().toISOString()) {
|
|
64
|
+
this.#scope = scope;
|
|
65
|
+
this.#secret = secret;
|
|
66
|
+
this.#now = now;
|
|
67
|
+
}
|
|
68
|
+
async accept(payload, signature, toEvent) {
|
|
69
|
+
const verified = verifySignature(payload, signature, this.#secret);
|
|
70
|
+
if (!verified.ok) {
|
|
71
|
+
// 튕긴 것도 사실이다. 아무도 안 오는 것과 오는데 다 튕기는 것을 구분해야 한다.
|
|
72
|
+
await this.#countRejected(verified.reason);
|
|
73
|
+
return { ok: false, reason: verified.reason, detail: verified.detail };
|
|
74
|
+
}
|
|
75
|
+
let event;
|
|
76
|
+
try {
|
|
77
|
+
event = toEvent(JSON.parse(payload));
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
await this.#countRejected('UNREADABLE');
|
|
81
|
+
return { ok: false, reason: 'UNREADABLE', detail: error instanceof Error ? error.message : String(error) };
|
|
82
|
+
}
|
|
83
|
+
if (!event) {
|
|
84
|
+
// 이 provider 사건은 우리가 아는 형태가 아니다 — 지어내지 않는다
|
|
85
|
+
await this.#countRejected('UNREADABLE');
|
|
86
|
+
return { ok: false, reason: 'UNREADABLE', detail: '읽을 수 있는 사건이 아니다' };
|
|
87
|
+
}
|
|
88
|
+
let seq = (await this.#lastSeq()) + 1;
|
|
89
|
+
for (;;) {
|
|
90
|
+
const record = IngressRecord.parse({ seq, receivedAt: this.#now(), event });
|
|
91
|
+
if (await this.#scope.setIfAbsent(bufferKey(seq), JSON.stringify(record)))
|
|
92
|
+
return { ok: true, seq };
|
|
93
|
+
seq += 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** 쌓인 것을 순서대로 꺼내고 지운다. 꺼낸 것은 Engine이 지고, 여기 남기지 않는다. */
|
|
97
|
+
async drain(limit = 100) {
|
|
98
|
+
const keys = (await this.#scope.keys(BUFFER_PREFIX)).sort().slice(0, limit);
|
|
99
|
+
const events = [];
|
|
100
|
+
for (const key of keys) {
|
|
101
|
+
const raw = await this.#scope.get(key);
|
|
102
|
+
if (!raw)
|
|
103
|
+
continue;
|
|
104
|
+
const parsed = IngressRecord.safeParse(JSON.parse(raw));
|
|
105
|
+
// 깨진 항목은 판정 근거가 못 된다 — 그 항목만 버린다
|
|
106
|
+
if (parsed.success)
|
|
107
|
+
events.push(parsed.data.event);
|
|
108
|
+
await this.#scope.delete(key);
|
|
109
|
+
}
|
|
110
|
+
return events;
|
|
111
|
+
}
|
|
112
|
+
async pending() {
|
|
113
|
+
return (await this.#scope.keys(BUFFER_PREFIX)).length;
|
|
114
|
+
}
|
|
115
|
+
/** 튕긴 수. 0이 아니면 무언가 잘못 설정돼 있다는 뜻이고, 그건 감지 공백이다. */
|
|
116
|
+
async rejected() {
|
|
117
|
+
const raw = await this.#scope.get(REJECTED_KEY);
|
|
118
|
+
return raw ? JSON.parse(raw) : {};
|
|
119
|
+
}
|
|
120
|
+
async #countRejected(reason) {
|
|
121
|
+
const current = await this.rejected();
|
|
122
|
+
await this.#scope.set(REJECTED_KEY, JSON.stringify({ ...current, [reason]: (current[reason] ?? 0) + 1 }));
|
|
123
|
+
}
|
|
124
|
+
async #lastSeq() {
|
|
125
|
+
const keys = await this.#scope.keys(BUFFER_PREFIX);
|
|
126
|
+
return keys.reduce((max, key) => Math.max(max, Number(key.slice(BUFFER_PREFIX.length)) || 0), 0);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 버퍼를 비우는 EventSource. **Engine은 이것이 push인지 pull인지 모른다** —
|
|
131
|
+
* `ports/event-source.ts` 가 예고한 바로 그 형태다.
|
|
132
|
+
*
|
|
133
|
+
* cursor를 쓰지 않는다: 꺼낸 것은 지워지므로 다음 회차는 남은 것부터 본다. 중복은
|
|
134
|
+
* 기존 dedupe(eventKey exact lookup)가 거른다 — 같은 변경을 회수 경로가 다시 봐도
|
|
135
|
+
* 패킷이 둘이 되지 않는 이유와 같다.
|
|
136
|
+
*/
|
|
137
|
+
export class IngressEventSource {
|
|
138
|
+
id;
|
|
139
|
+
#ingress;
|
|
140
|
+
#limit;
|
|
141
|
+
constructor(deps) {
|
|
142
|
+
this.id = deps.id ?? 'webhook-ingress';
|
|
143
|
+
this.#ingress = deps.ingress;
|
|
144
|
+
this.#limit = deps.limit ?? 100;
|
|
145
|
+
}
|
|
146
|
+
async drain(_cursor) {
|
|
147
|
+
const events = await this.#ingress.drain(this.#limit);
|
|
148
|
+
return { events, cursor: null, hasMore: (await this.#ingress.pending()) > 0 };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 어느 문으로 들어왔는가 (C-14 §3.4).
|
|
4
|
+
*
|
|
5
|
+
* **`bootstrap` 만 stable runtime 설치를 계획한다.** 설치된 runtime이 스스로를 다시
|
|
6
|
+
* 설치하는 것은 말이 안 되고, 그것을 계획에 넣으면 매 setup이 npm을 부른다.
|
|
7
|
+
*/
|
|
8
|
+
export type AscEntry = 'runtime' | 'bootstrap';
|
|
9
|
+
/**
|
|
10
|
+
* CLI 한 번의 실행. **다른 진입도 이 함수를 부른다** (C-14 불변식 ①).
|
|
11
|
+
*
|
|
12
|
+
* bootstrap 패키지는 아직 아무것도 설치되지 않은 machine에서 이것을 그대로 부른다 —
|
|
13
|
+
* 진입이 둘이어도 판단은 하나다. 그래서 export이고, 그래서 아래 자동 실행은 이 파일이
|
|
14
|
+
* 진짜 진입점일 때만 돈다.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runAscCommand(argv: string[], entry?: AscEntry): Promise<number>;
|