@asc-agent/runtime 0.5.0 → 0.5.1
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/README.md +1 -1
- package/dist/adapters/claude-code/guard.js +26 -1
- package/dist/adapters/claude-code/install.d.ts +27 -0
- package/dist/adapters/claude-code/install.js +3 -1
- package/dist/cli/asc.js +328 -14
- package/dist/composition/runtime.d.ts +17 -0
- package/dist/composition/runtime.js +26 -0
- package/dist/core/attach/setup-plan.d.ts +107 -6
- package/dist/core/attach/setup-plan.js +104 -6
- package/dist/core/attach/setup.d.ts +3 -1
- package/dist/core/attach/setup.js +25 -1
- package/dist/core/distribution/release.d.ts +3 -3
- package/dist/core/distribution/release.js +1 -1
- package/dist/core/distribution/service-runtime.d.ts +40 -0
- package/dist/core/distribution/service-runtime.js +76 -0
- package/dist/schemas/profile.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ proceed-by-default, escalation, audit, the external-write guard, host integratio
|
|
|
7
7
|
here.
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install -g @asc-agent/runtime@0.5.
|
|
10
|
+
npm install -g @asc-agent/runtime@0.5.1
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
npm owns the executable link (on Windows, npm's own `asc.cmd`). This package never edits
|
|
@@ -231,7 +231,11 @@ try {
|
|
|
231
231
|
process.exit(0) // 입력을 못 읽으면 판단하지 않는다 — guard 오작동으로 전부 막는 것이 더 나쁘다
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
// 파일을 바꾸는 도구는 **일을 시작한다는 신호**다 (F6). 읽기는 여기 없다 — 상태를 보는
|
|
235
|
+
// 세션까지 관리 대상으로 끌어들이면 그것은 자동화가 아니라 방해다.
|
|
236
|
+
const MUTATORS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])
|
|
237
|
+
const isMutation = MUTATORS.has(String(input.tool_name ?? ''))
|
|
238
|
+
if (input.tool_name !== 'Bash' && !isMutation) process.exit(0)
|
|
235
239
|
const command = String(input.tool_input?.command ?? '')
|
|
236
240
|
|
|
237
241
|
const cwd = input.cwd ?? process.cwd()
|
|
@@ -259,6 +263,24 @@ const ascRoot = registered ? registered.root : findAscRoot(cwd)
|
|
|
259
263
|
if (!ascRoot) process.exit(0)
|
|
260
264
|
|
|
261
265
|
const managed = findManaged(ascRoot, observedSessionId)
|
|
266
|
+
|
|
267
|
+
// **일이 시작되는데 논리 세션이 없다** (F6). 사람이 "ASC 적용해" 라고 말해야 했던 자리다.
|
|
268
|
+
// 여기서 막고 다음 한 걸음을 그대로 준다 — 그 명령을 실행하는 것은 agent 이고, 사람이
|
|
269
|
+
// 아니다. 세션에 들어간 뒤에는 이 문이 다시 열린다.
|
|
270
|
+
if (isMutation && !managed) {
|
|
271
|
+
const id = observedSessionId || '<this session id>'
|
|
272
|
+
console.error(
|
|
273
|
+
[
|
|
274
|
+
'[ASC] 이 workspace 는 ASC 가 관리한다. 파일을 바꾸기 전에 논리 세션 안에 들어가라.',
|
|
275
|
+
' asc proceed --work <WORK-KEY> --json # 작업 항목이 있으면',
|
|
276
|
+
' asc proceed --json # 이어갈 세션을 고르거나 계약을 제안받는다',
|
|
277
|
+
' asc host claude bind <S-ID> --physical ' + id,
|
|
278
|
+
'읽기·조회는 막지 않는다 — 막는 것은 관리 밖의 변경뿐이다.',
|
|
279
|
+
].join('\\n'),
|
|
280
|
+
)
|
|
281
|
+
process.exit(2)
|
|
282
|
+
}
|
|
283
|
+
|
|
262
284
|
if (!managed) process.exit(0)
|
|
263
285
|
|
|
264
286
|
// 관찰은 여기서 끝난다 — 아래 차단 판정은 이 호출의 성패를 보지 않는다
|
|
@@ -266,6 +288,9 @@ try {
|
|
|
266
288
|
recordActivity(ascRoot, managed, observedSessionId, String(input.tool_name ?? ''))
|
|
267
289
|
} catch {}
|
|
268
290
|
|
|
291
|
+
// 변경 도구는 여기까지다 — 아래 목록은 Bash 명령에 대한 것이다.
|
|
292
|
+
if (isMutation) process.exit(0)
|
|
293
|
+
|
|
269
294
|
for (const { pattern, label } of FORBIDDEN) {
|
|
270
295
|
if (pattern.test(command)) {
|
|
271
296
|
console.error(
|
|
@@ -10,6 +10,32 @@ export type InstallPaths = {
|
|
|
10
10
|
entry?: string;
|
|
11
11
|
};
|
|
12
12
|
export declare const defaultPaths: () => InstallPaths;
|
|
13
|
+
/**
|
|
14
|
+
* ASC가 등록하는 hook들. 이벤트마다 **하나**씩이며 중복 등록은 그 자체가 결함이다.
|
|
15
|
+
*
|
|
16
|
+
* `_asc` 표식이 소유권의 근거다 — 이것이 붙은 항목만 우리가 고치고 지운다.
|
|
17
|
+
*/
|
|
18
|
+
type HookSpec = {
|
|
19
|
+
event: string;
|
|
20
|
+
marker: string;
|
|
21
|
+
/** 이 이벤트가 matcher를 쓰는가. SessionStart는 도구 이름으로 거르지 않는다. */
|
|
22
|
+
matcher?: string;
|
|
23
|
+
script: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function locate(paths: InstallPaths): {
|
|
26
|
+
/** Bundle 전체. 파일이 늘어도 아래 계약(manifest·digest·멱등)은 그대로다 (C-05 §5). */
|
|
27
|
+
skills: {
|
|
28
|
+
name: string;
|
|
29
|
+
path: string;
|
|
30
|
+
text: string;
|
|
31
|
+
}[];
|
|
32
|
+
/** hook은 **하나**로 둔다. guard는 안전 층이고 중복 등록은 그 자체가 위험이다. */
|
|
33
|
+
hook: string;
|
|
34
|
+
front: string;
|
|
35
|
+
settings: string;
|
|
36
|
+
manifest: string;
|
|
37
|
+
hooks: HookSpec[];
|
|
38
|
+
};
|
|
13
39
|
export type InstallOutcome = {
|
|
14
40
|
written: string[];
|
|
15
41
|
skipped: {
|
|
@@ -72,3 +98,4 @@ export type UninstallOutcome = {
|
|
|
72
98
|
};
|
|
73
99
|
/** manifest로 증명되는 것만 제거한다. 사용자가 고친 파일은 남기고 이유를 말한다. */
|
|
74
100
|
export declare function uninstall(paths: InstallPaths): Promise<UninstallOutcome>;
|
|
101
|
+
export {};
|
|
@@ -20,7 +20,7 @@ const sha = (text) => createHash('sha256').update(text).digest('hex').slice(0, 1
|
|
|
20
20
|
export const defaultPaths = () => ({ claudeHome: join(homedir(), '.claude') });
|
|
21
21
|
const HOOK_MARKER = 'asc-external-write-guard';
|
|
22
22
|
const FRONT_MARKER = 'asc-front-binding';
|
|
23
|
-
function locate(paths) {
|
|
23
|
+
export function locate(paths) {
|
|
24
24
|
const guard = join(paths.claudeHome, 'asc', 'guard-hook.mjs');
|
|
25
25
|
const front = join(paths.claudeHome, 'asc', 'front-hook.mjs');
|
|
26
26
|
return {
|
|
@@ -38,6 +38,8 @@ function locate(paths) {
|
|
|
38
38
|
hooks: (() => {
|
|
39
39
|
const specs = [
|
|
40
40
|
{ event: 'PreToolUse', marker: HOOK_MARKER, matcher: 'Bash', script: guard },
|
|
41
|
+
// 파일을 바꾸는 도구도 같은 문을 지난다 — 일이 시작되는 신호이기 때문이다 (F6).
|
|
42
|
+
{ event: 'PreToolUse', marker: HOOK_MARKER, matcher: 'Edit|Write|MultiEdit|NotebookEdit', script: guard },
|
|
41
43
|
];
|
|
42
44
|
// 부를 곳을 모르면 심지 않는다 (§InstallPaths.entry)
|
|
43
45
|
if (paths.entry)
|
package/dist/cli/asc.js
CHANGED
|
@@ -14,7 +14,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
14
14
|
import { homedir, hostname, userInfo } from 'node:os';
|
|
15
15
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
16
16
|
import { MINIMUM_NODE_MAJOR, checkNodeRuntime, reexecWithCandidate, } from "../core/distribution/node-runtime.js";
|
|
17
|
-
import { RELEASE_VERSION } from "../core/distribution/release.js";
|
|
17
|
+
import { RELEASE_VERSION, RUNTIME_PACKAGE } from "../core/distribution/release.js";
|
|
18
18
|
import { GitHubClient, discoverToken } from "../adapters/github/client.js";
|
|
19
19
|
import { GitHubChangeContext, GitHubInventory, GitHubResourceContext } from "../adapters/github/context.js";
|
|
20
20
|
import { GitHubEventSource } from "../adapters/github/event-source.js";
|
|
@@ -47,6 +47,7 @@ import { evaluateHealth, healthAlertLines } from "../core/monitor/health-alerts.
|
|
|
47
47
|
import { Operator } from "../core/operator/proceed.js";
|
|
48
48
|
import { deriveSessionContractDraft } from "../core/operator/derive-draft.js";
|
|
49
49
|
import { servicePath } from "../core/distribution/external-command.js";
|
|
50
|
+
import { isTransientPath, resolveServiceRuntime, serviceRuntimeLine, } from "../core/distribution/service-runtime.js";
|
|
50
51
|
import { LocalCanonicalReader } from "../adapters/local/canonical.js";
|
|
51
52
|
import { LocalRepoAdapter } from "../adapters/local/repo.js";
|
|
52
53
|
import { GitHubAdapter } from "../adapters/github/adapter.js";
|
|
@@ -56,7 +57,7 @@ import { healJam, healLine } from "../adapters/jam/setup.js";
|
|
|
56
57
|
import { statusIndicatesDone } from "../adapters/jam/ports.js";
|
|
57
58
|
import { ProgressService } from "../core/operator/progress.js";
|
|
58
59
|
import { composeBindings, defaultAdapters } from "../composition/registry.js";
|
|
59
|
-
import { buildObservationChannels, buildRuntimePorts, closeToolClients, rolesFor } from "../composition/runtime.js";
|
|
60
|
+
import { buildObservationChannels, buildRuntimePorts, closeToolClients, rolesFor, workItemRoles, } from "../composition/runtime.js";
|
|
60
61
|
import { proposeBindings } from "../composition/propose.js";
|
|
61
62
|
import { buildEventObservation } from "../composition/observe.js";
|
|
62
63
|
import { availableProfiles, planBootstrap, renderPlan } from "../core/attach/bootstrap.js";
|
|
@@ -1331,6 +1332,207 @@ async function runSetup(command, values, entry = 'runtime') {
|
|
|
1331
1332
|
* Core의 planner는 파일도 network도 모른다 — 사실은 여기서 관측해 넘긴다. 그래야
|
|
1332
1333
|
* "이 명령이 무엇을 바꿀 것인가"를 아무것도 바꾸지 않고 물어볼 수 있다.
|
|
1333
1334
|
*/
|
|
1335
|
+
/**
|
|
1336
|
+
* 이 저장소가 스스로 증명하는 Profile 이름 (P0 F2).
|
|
1337
|
+
*
|
|
1338
|
+
* **파일을 만들지 않는다** — 이름과 "이미 있는가"만 관측한다. 다른 논리 workspace 를
|
|
1339
|
+
* 찾아 합치는 것이 아니라, 이 checkout 의 remote 하나에서 읽히는 신원이다 (C-11 유지).
|
|
1340
|
+
*/
|
|
1341
|
+
async function adoptableState(projectRoot, git) {
|
|
1342
|
+
if (!git)
|
|
1343
|
+
return {};
|
|
1344
|
+
const remotes = await gitRemotes(projectRoot);
|
|
1345
|
+
if (remotes.length === 0)
|
|
1346
|
+
return {};
|
|
1347
|
+
try {
|
|
1348
|
+
const adopted = buildAdoptedProfile({
|
|
1349
|
+
dirName: basename(projectRoot),
|
|
1350
|
+
remotes,
|
|
1351
|
+
scmForHost: (host) => (host === 'github.com' ? 'github' : 'git'),
|
|
1352
|
+
});
|
|
1353
|
+
// **공유 주소가 없으면 후보가 아니다.** remote 가 프로젝트 신원을 증명하지 못하면
|
|
1354
|
+
// 그것은 이 기계 안의 폴더일 뿐이고, 무엇으로 붙을지는 사람이 정한다.
|
|
1355
|
+
const project = adopted.profile.project;
|
|
1356
|
+
if (!project?.repository || project.repository.startsWith('local/'))
|
|
1357
|
+
return {};
|
|
1358
|
+
const exists = existsSync(join(externalProfileRoot(), adopted.id, 'profile.json'));
|
|
1359
|
+
return { adoptable: { id: adopted.id, exists } };
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
// 이름을 만들 수 없는 저장소가 있다. 그때는 이 축이 없는 것이고, 사람이 고른다.
|
|
1363
|
+
return {};
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* 승인 권한자가 서 있는가, 그리고 지금 이 사람을 무엇으로 부를 수 있는가 (P0 F2).
|
|
1368
|
+
*
|
|
1369
|
+
* **인증된 provider 가 말한 이름을 쓴다.** ASC 가 지어내지 않고, 자격 값은 읽지 않는다.
|
|
1370
|
+
*/
|
|
1371
|
+
async function identityState(ascRoot, workspaceComing) {
|
|
1372
|
+
const wired = ascRoot ? Object.keys(await loadIdentityMap(ascRoot)).length > 0 : false;
|
|
1373
|
+
if (wired)
|
|
1374
|
+
return { identity: { wired: true } };
|
|
1375
|
+
// **세울 workspace 가 없으면 누구인지 묻지 않는다.** 물으려면 provider CLI 를 불러야
|
|
1376
|
+
// 하고, 그 도구는 자기 설정 파일을 만든다 — 멈출 계획이 남기는 자국이 되면 안 된다.
|
|
1377
|
+
if (!workspaceComing)
|
|
1378
|
+
return { identity: { wired: false } };
|
|
1379
|
+
const actor = await detectActor();
|
|
1380
|
+
return { identity: { wired: false, ...(actor ? { actor } : {}) } };
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* 지금 이 사람을 `<channel>:<actor>` 로. 인증된 통로가 먼저다 — 그것이 실제로 밖에서
|
|
1384
|
+
* 나를 부르는 이름이고, git 의 표시 이름은 아무 계정과도 이어지지 않는다.
|
|
1385
|
+
*/
|
|
1386
|
+
async function detectActor() {
|
|
1387
|
+
const gitlab = await execText('glab', ['api', 'user']);
|
|
1388
|
+
const gitlabName = readJsonField(gitlab, 'username');
|
|
1389
|
+
if (gitlabName)
|
|
1390
|
+
return `gitlab:${gitlabName}`;
|
|
1391
|
+
const github = await execText('gh', ['api', 'user']);
|
|
1392
|
+
const githubName = readJsonField(github, 'login');
|
|
1393
|
+
if (githubName)
|
|
1394
|
+
return `github:${githubName}`;
|
|
1395
|
+
const local = await detectSelf();
|
|
1396
|
+
return local ? `local:${local}` : null;
|
|
1397
|
+
}
|
|
1398
|
+
function readJsonField(text, field) {
|
|
1399
|
+
if (!text)
|
|
1400
|
+
return null;
|
|
1401
|
+
try {
|
|
1402
|
+
const value = JSON.parse(text)[field];
|
|
1403
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
1404
|
+
}
|
|
1405
|
+
catch {
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* 발견이 증명하는 결합 (P0 F2).
|
|
1411
|
+
*
|
|
1412
|
+
* **역할은 capability 로 정한다** — provider 이름으로 정하면 이 자리가 provider 목록이
|
|
1413
|
+
* 된다. 변경을 아는 통로가 code, 작업 항목을 세는 통로가 work 다. 같은 역할에 후보가
|
|
1414
|
+
* 둘이면 origin remote 가 가리키는 쪽을 쓰고, 그것으로도 갈리면 아무것도 제안하지 않는다.
|
|
1415
|
+
*/
|
|
1416
|
+
async function bindingProposalState(projectRoot, profileId, willCreate) {
|
|
1417
|
+
if (!profileId)
|
|
1418
|
+
return {};
|
|
1419
|
+
// 이미 선언이 있으면 손대지 않는다 — 사람이 적은 것이 먼저다.
|
|
1420
|
+
// 파일이 아직 없어도 **이번에 만들 것이면** 비어 있는 것과 같다: adopt 는 결합을
|
|
1421
|
+
// 적지 않으므로, 그 사실을 여기서 미리 알아야 한 번의 apply 로 끝난다.
|
|
1422
|
+
const declared = await readProfileBindings(profileId);
|
|
1423
|
+
if (declared === null && !willCreate)
|
|
1424
|
+
return {};
|
|
1425
|
+
if (declared !== null && declared.length > 0)
|
|
1426
|
+
return {};
|
|
1427
|
+
const adapters = monitorAdapters();
|
|
1428
|
+
const plan = await composeBindings({ context: { projectRoot, env: process.env }, adapters, roles: [] });
|
|
1429
|
+
const usable = plan.bindings.filter((binding) => binding.state === 'AVAILABLE' || binding.state === 'DEGRADED');
|
|
1430
|
+
const origin = (await gitRemotes(projectRoot)).find((remote) => remote.name === 'origin');
|
|
1431
|
+
const originAlias = origin ? normalizeRemote(origin.url) : null;
|
|
1432
|
+
const byRole = new Map();
|
|
1433
|
+
for (const binding of usable) {
|
|
1434
|
+
const role = binding.provides.includes('context.change')
|
|
1435
|
+
? 'code-primary'
|
|
1436
|
+
: binding.provides.includes('inventory.enumerate')
|
|
1437
|
+
? 'work'
|
|
1438
|
+
: null;
|
|
1439
|
+
if (!role)
|
|
1440
|
+
continue;
|
|
1441
|
+
byRole.set(role, [...(byRole.get(role) ?? []), { role, adapter: binding.adapterId, resource: binding.resource }]);
|
|
1442
|
+
}
|
|
1443
|
+
const proposal = [];
|
|
1444
|
+
for (const [role, candidates] of byRole) {
|
|
1445
|
+
if (candidates.length === 1) {
|
|
1446
|
+
proposal.push(candidates[0]);
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
// origin 이 가리키는 것 하나면 그것이다 — 이 저장소의 주소가 그 판정의 근거다.
|
|
1450
|
+
const matching = originAlias
|
|
1451
|
+
? candidates.filter((candidate) => originAlias.endsWith(`/${candidate.resource}`))
|
|
1452
|
+
: [];
|
|
1453
|
+
if (matching.length === 1)
|
|
1454
|
+
proposal.push(matching[0]);
|
|
1455
|
+
else
|
|
1456
|
+
return {};
|
|
1457
|
+
}
|
|
1458
|
+
return proposal.length > 0 ? { bindingProposal: proposal } : {};
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* 발견이 증명한 결합을 Profile 파일에 적는다.
|
|
1462
|
+
*
|
|
1463
|
+
* **이미 있는 선언은 건드리지 않는다** — 사람이 적은 것이 먼저다. 그 밖의 필드도 그대로
|
|
1464
|
+
* 둔다: 이 함수가 아는 것은 `bindings` 한 칸뿐이다.
|
|
1465
|
+
*/
|
|
1466
|
+
async function writeProfileBindings(profileId, bindings) {
|
|
1467
|
+
const path = join(externalProfileRoot(), profileId, 'profile.json');
|
|
1468
|
+
const profile = JSON.parse(await readFile(path, 'utf8'));
|
|
1469
|
+
if (Array.isArray(profile.bindings) && profile.bindings.length > 0)
|
|
1470
|
+
return false;
|
|
1471
|
+
profile.bindings = bindings.map((binding) => ({ ...binding }));
|
|
1472
|
+
await writeFile(path, `${JSON.stringify(profile, null, 2)}\n`, 'utf8');
|
|
1473
|
+
console.log(`bindings declared in ${profileId}: ${bindings.map((b) => `${b.role}=${b.adapter}`).join(', ')}`);
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* 이 checkout 이 증명하는 정본 갈래 (P0 F5).
|
|
1478
|
+
*
|
|
1479
|
+
* **remote 에게 물어본다.** 로컬 `origin/HEAD` 는 clone 시점에 고정돼 낡는다 — 이 저장소에서
|
|
1480
|
+
* 그 값은 `main` 인데 remote 의 기본 branch 는 `develop` 이었다. 낡은 값을 정본으로 적으면
|
|
1481
|
+
* 세션이 엉뚱한 baseline 을 딛는다. 물어보지 못하면 적지 않는다.
|
|
1482
|
+
*/
|
|
1483
|
+
async function canonicalProposalState(projectRoot, git, profileId, willCreate) {
|
|
1484
|
+
if (!git || !profileId)
|
|
1485
|
+
return {};
|
|
1486
|
+
const declared = await readProfileCanonical(profileId);
|
|
1487
|
+
if (declared === null && !willCreate)
|
|
1488
|
+
return {};
|
|
1489
|
+
if (declared !== null && declared > 0)
|
|
1490
|
+
return {};
|
|
1491
|
+
const symref = await execText('git', ['-C', projectRoot, 'ls-remote', '--symref', 'origin', 'HEAD']);
|
|
1492
|
+
const match = symref ? /^ref:\s+refs\/heads\/(\S+)\s+HEAD$/m.exec(symref) : null;
|
|
1493
|
+
const branch = match?.[1];
|
|
1494
|
+
if (!branch)
|
|
1495
|
+
return {};
|
|
1496
|
+
// provider 는 `git` 이다 — 이 값은 checkout 이 이미 들고 있는 사실이고, 그것을 읽는 통로가
|
|
1497
|
+
// 선언된 provider 를 따른다 (canonical baseline 읽기).
|
|
1498
|
+
return { canonicalProposal: { id: branch, provider: 'git', remote: 'origin', ref: branch } };
|
|
1499
|
+
}
|
|
1500
|
+
/** Profile 이 선언한 정본 갈래 수. 파일이 없으면 `null`. */
|
|
1501
|
+
async function readProfileCanonical(profileId) {
|
|
1502
|
+
const path = join(externalProfileRoot(), profileId, 'profile.json');
|
|
1503
|
+
try {
|
|
1504
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
1505
|
+
return Array.isArray(parsed.canonical?.sources) ? parsed.canonical.sources.length : 0;
|
|
1506
|
+
}
|
|
1507
|
+
catch {
|
|
1508
|
+
return null;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* remote 가 말한 정본 갈래를 Profile 에 적는다. 이미 선언이 있으면 손대지 않는다.
|
|
1513
|
+
*/
|
|
1514
|
+
async function writeProfileCanonical(profileId, source) {
|
|
1515
|
+
const path = join(externalProfileRoot(), profileId, 'profile.json');
|
|
1516
|
+
const profile = JSON.parse(await readFile(path, 'utf8'));
|
|
1517
|
+
const canonical = (profile.canonical ?? {});
|
|
1518
|
+
if (Array.isArray(canonical.sources) && canonical.sources.length > 0)
|
|
1519
|
+
return false;
|
|
1520
|
+
profile.canonical = { ...canonical, sources: [source] };
|
|
1521
|
+
await writeFile(path, `${JSON.stringify(profile, null, 2)}\n`, 'utf8');
|
|
1522
|
+
console.log(`canonical source declared in ${profileId}: ${source.remote}/${source.ref}`);
|
|
1523
|
+
return true;
|
|
1524
|
+
}
|
|
1525
|
+
/** Profile 이 이미 선언한 결합. 파일이 없으면 `null` — 없는 것과 비어 있는 것은 다르다. */
|
|
1526
|
+
async function readProfileBindings(profileId) {
|
|
1527
|
+
const path = join(externalProfileRoot(), profileId, 'profile.json');
|
|
1528
|
+
try {
|
|
1529
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
1530
|
+
return Array.isArray(parsed.bindings) ? parsed.bindings : [];
|
|
1531
|
+
}
|
|
1532
|
+
catch {
|
|
1533
|
+
return null;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1334
1536
|
async function detectSetupState(values, entry) {
|
|
1335
1537
|
const { root: projectRoot, git } = await discoverProjectRoot(process.cwd());
|
|
1336
1538
|
const resolution = await resolveRoot(process.cwd(), values.root);
|
|
@@ -1340,6 +1542,10 @@ async function detectSetupState(values, entry) {
|
|
|
1340
1542
|
const attachmentBroken = ascRoot ? (await inspectSetup(ascRoot)).attachment === 'BROKEN' : false;
|
|
1341
1543
|
const scope = values.scope === 'project' ? 'project' : 'local';
|
|
1342
1544
|
const hostReport = await verifyInstall(hostPaths());
|
|
1545
|
+
const adoptable = await adoptableState(projectRoot, git);
|
|
1546
|
+
const attachedProfile = ascRoot ? await lockedProfileId(ascRoot) : undefined;
|
|
1547
|
+
// 이번 계획이 쓸 Profile 하나 — 그것의 결합 선언이 비어 있을 때만 제안을 관측한다.
|
|
1548
|
+
const targetProfile = values.profile ?? attachedProfile ?? adoptable.adoptable?.id;
|
|
1343
1549
|
return {
|
|
1344
1550
|
entry,
|
|
1345
1551
|
projectRoot,
|
|
@@ -1349,6 +1555,12 @@ async function detectSetupState(values, entry) {
|
|
|
1349
1555
|
...(values.profile ? { requestedProfile: values.profile } : {}),
|
|
1350
1556
|
profileCandidates: await availableProfiles(installRoot(), externalProfileRoot()),
|
|
1351
1557
|
scope,
|
|
1558
|
+
...(attachedProfile ? { attachedProfile } : {}),
|
|
1559
|
+
// fresh onboarding 이 사람에게 되묻지 않으려면 이 셋이 관측돼 있어야 한다 (P0 F2).
|
|
1560
|
+
...adoptable,
|
|
1561
|
+
...(await identityState(ascRoot, Boolean(ascRoot) || Boolean(targetProfile))),
|
|
1562
|
+
...(await bindingProposalState(projectRoot, targetProfile, Boolean(adoptable.adoptable && !adoptable.adoptable.exists))),
|
|
1563
|
+
...(await canonicalProposalState(projectRoot, git, targetProfile, Boolean(adoptable.adoptable && !adoptable.adoptable.exists))),
|
|
1352
1564
|
host: [{ id: 'claude', status: hostReport.status }],
|
|
1353
1565
|
// Profile 이 작업 도구를 선언했으면 그 준비 상태까지 본다 (설계 §9.3).
|
|
1354
1566
|
...(await workBindingState(ascRoot, projectRoot)),
|
|
@@ -1367,7 +1579,10 @@ async function detectSetupState(values, entry) {
|
|
|
1367
1579
|
* 스스로 할 때만 의미가 있다.
|
|
1368
1580
|
*/
|
|
1369
1581
|
function runningFromInstalledPackage() {
|
|
1370
|
-
|
|
1582
|
+
const here = fileURLToPath(import.meta.url).replace(/\\/g, '/');
|
|
1583
|
+
// **npx 캐시도 node_modules 다.** 경로 모양만 보면 임시 자리가 설치본으로 통과하고,
|
|
1584
|
+
// 실제로 그렇게 통과해 등록물이 npx 캐시를 가리켰다 — 캐시를 지우면 깨진다.
|
|
1585
|
+
return here.includes('/node_modules/@asc-agent/runtime/') && !isTransientPath(here);
|
|
1371
1586
|
}
|
|
1372
1587
|
/**
|
|
1373
1588
|
* 지금 이 실행에서 기계 등록을 다뤄도 되는가.
|
|
@@ -1394,7 +1609,13 @@ async function persistentRuntimeState(values) {
|
|
|
1394
1609
|
const adapter = serviceAdapter();
|
|
1395
1610
|
if (!adapter)
|
|
1396
1611
|
return {};
|
|
1397
|
-
|
|
1612
|
+
// 등록물이 가리킬 자리가 없으면 이 축을 그리지 않는다 — 깨진 등록을 남기느니 등록하지
|
|
1613
|
+
// 않고 그 사실을 말한다 (P0 F1).
|
|
1614
|
+
const runtime = await serviceRuntime();
|
|
1615
|
+
if (runtime.kind !== 'STABLE') {
|
|
1616
|
+
return { persistentRuntime: { action: 'unsupported', adapter: adapter.id, detail: runtime.detail } };
|
|
1617
|
+
}
|
|
1618
|
+
const plan = await planPersistentRuntime(adapter, serviceCommand(serviceInterval(values), runtime)).catch(() => null);
|
|
1398
1619
|
if (!plan)
|
|
1399
1620
|
return {};
|
|
1400
1621
|
return {
|
|
@@ -1635,6 +1856,14 @@ async function runSetupLifecycle(command, values, entry) {
|
|
|
1635
1856
|
const speak = console.log;
|
|
1636
1857
|
if (asJson)
|
|
1637
1858
|
console.log = console.error;
|
|
1859
|
+
const relock = async (profile) => {
|
|
1860
|
+
const root = await discoverRoot(process.cwd(), values.root);
|
|
1861
|
+
if (!root)
|
|
1862
|
+
return;
|
|
1863
|
+
const code = await runProfile('resolve', { ...values, profile, write: true }, root);
|
|
1864
|
+
if (code !== 0)
|
|
1865
|
+
throw new Error(`profile re-lock 실패 (exit ${code})`);
|
|
1866
|
+
};
|
|
1638
1867
|
let outcome;
|
|
1639
1868
|
try {
|
|
1640
1869
|
outcome = await applySetupPlan(plan, {
|
|
@@ -1657,8 +1886,13 @@ async function runSetupLifecycle(command, values, entry) {
|
|
|
1657
1886
|
const adapter = serviceAdapter();
|
|
1658
1887
|
if (!adapter)
|
|
1659
1888
|
return;
|
|
1889
|
+
const runtime = await serviceRuntime();
|
|
1890
|
+
if (runtime.kind !== 'STABLE') {
|
|
1891
|
+
console.error(`Persistent runtime not registered — ${runtime.detail}`);
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1660
1894
|
try {
|
|
1661
|
-
await adapter.install(serviceCommand(serviceInterval(values)));
|
|
1895
|
+
await adapter.install(serviceCommand(serviceInterval(values), runtime));
|
|
1662
1896
|
console.log(`Persistent runtime registered with ${adapter.id}.`);
|
|
1663
1897
|
}
|
|
1664
1898
|
catch (error) {
|
|
@@ -1680,8 +1914,41 @@ async function runSetupLifecycle(command, values, entry) {
|
|
|
1680
1914
|
if (code !== 0)
|
|
1681
1915
|
throw new Error(`${change.host} 설치 실패 (exit ${code})`);
|
|
1682
1916
|
},
|
|
1917
|
+
// 이 저장소를 설명하는 Profile 을 만든다. 이름은 plan 이 이미 정했다.
|
|
1918
|
+
adoptProfile: async (change) => {
|
|
1919
|
+
const code = await runProfileAdopt({ ...values, id: change.profile, json: true }, entry);
|
|
1920
|
+
if (code !== 0)
|
|
1921
|
+
throw new Error(`profile adopt 실패 (exit ${code})`);
|
|
1922
|
+
},
|
|
1923
|
+
// 승인 권한자를 세운다. 이름과 채널뿐이고 비밀은 다루지 않는다.
|
|
1924
|
+
wireIdentity: async (change) => {
|
|
1925
|
+
const code = await runSetupIdentity({ ...values, actor: change.actor, role: 'both' });
|
|
1926
|
+
if (code !== 0)
|
|
1927
|
+
throw new Error(`identity 결선 실패 (exit ${code})`);
|
|
1928
|
+
},
|
|
1929
|
+
// 발견이 증명한 결합을 Profile 에 적는다. 갈리는 것은 plan 에 들어오지 않는다.
|
|
1930
|
+
declareCanonical: async (change) => {
|
|
1931
|
+
const written = await writeProfileCanonical(change.profile, change.source);
|
|
1932
|
+
if (written)
|
|
1933
|
+
await relock(change.profile);
|
|
1934
|
+
},
|
|
1935
|
+
declareBindings: async (change) => {
|
|
1936
|
+
const written = await writeProfileBindings(change.profile, change.bindings);
|
|
1937
|
+
// Profile 을 고쳤으면 lock 이 어긋난다 — 다음 명령이 그 drift 앞에서 멈춘다.
|
|
1938
|
+
// 고친 쪽이 닫는다 (setup identity 가 하는 것과 같다).
|
|
1939
|
+
if (written)
|
|
1940
|
+
await relock(change.profile);
|
|
1941
|
+
},
|
|
1683
1942
|
});
|
|
1684
1943
|
}
|
|
1944
|
+
catch (error) {
|
|
1945
|
+
// **적용 실패는 stack trace 가 아니라 답이어야 한다.** agent 는 이 문서를 읽고 다음
|
|
1946
|
+
// 행동을 정한다 — 예외가 그대로 나가면 stdout 이 비고 아무것도 판단할 수 없다.
|
|
1947
|
+
console.log = speak;
|
|
1948
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1949
|
+
emit({ ...plan, status: 'apply_failed', changesApplied: false, detail });
|
|
1950
|
+
return 1;
|
|
1951
|
+
}
|
|
1685
1952
|
finally {
|
|
1686
1953
|
console.log = speak;
|
|
1687
1954
|
}
|
|
@@ -1719,6 +1986,7 @@ async function inspectSetup(root) {
|
|
|
1719
1986
|
...(runtime
|
|
1720
1987
|
? { profile: { id: runtime.layers.profile.id, origin: runtime.layers.profileOrigin } }
|
|
1721
1988
|
: {}),
|
|
1989
|
+
...(runtime ? { canonicalSources: runtime.layers.profile.canonical.sources.length } : {}),
|
|
1722
1990
|
hasApprovers: Object.keys(await loadIdentityMap(root)).length > 0,
|
|
1723
1991
|
hasControllerIdentities: Object.keys(runtime?.controllerIdentities ?? {}).length > 0,
|
|
1724
1992
|
hasMonitorIdentities: (runtime?.monitor.identities?.length ?? 0) > 0,
|
|
@@ -2201,6 +2469,7 @@ async function runProceed(values, store, root, resolved) {
|
|
|
2201
2469
|
const workRef = values.work ?? undefined;
|
|
2202
2470
|
const ingress = workRef ? await buildWorkIngress(store, root, sessions, resolved) : undefined;
|
|
2203
2471
|
if (workRef && !ingress) {
|
|
2472
|
+
// **정본이 없는 것과 다른 실패다.** 정본은 code 쪽 사실이고 이것은 작업 항목 쪽이다.
|
|
2204
2473
|
console.error(`작업 항목 '${workRef}' 을 읽을 통로가 없다 — Profile bindings 에 작업 항목 provider 를 선언하라.`);
|
|
2205
2474
|
return 2;
|
|
2206
2475
|
}
|
|
@@ -2416,7 +2685,11 @@ async function buildWorkIngress(store, root, runtime, resolved) {
|
|
|
2416
2685
|
plan,
|
|
2417
2686
|
// 제안은 **말하는 것**이지 정하는 것이 아니다. 역할을 박아 넣으면 선언과 구분되지 않고,
|
|
2418
2687
|
// capability 해석은 후보가 유일할 때 이미 스스로 풀린다.
|
|
2419
|
-
|
|
2688
|
+
//
|
|
2689
|
+
// 다만 **작업 항목을 누구에게 물을지는 여기서 정한다** — code 와 work 가 둘 다 자원
|
|
2690
|
+
// 조회를 제공하면 `rolesFor` 는 아무것도 정하지 못하고, 그러면 선언해 둔 work binding 이
|
|
2691
|
+
// 있는데도 "통로가 없다" 가 된다 (P0 F7).
|
|
2692
|
+
roles: { ...rolesFor(plan, declared), ...workItemRoles(plan, declared) },
|
|
2420
2693
|
perPage: 30,
|
|
2421
2694
|
...jamComposition(projectRoot),
|
|
2422
2695
|
endpointFor: (binding) => endpointOf(adapters, binding),
|
|
@@ -3509,7 +3782,11 @@ async function serviceHealth(values) {
|
|
|
3509
3782
|
const adapter = serviceAdapter();
|
|
3510
3783
|
if (!adapter)
|
|
3511
3784
|
return null;
|
|
3512
|
-
const
|
|
3785
|
+
const runtime = await serviceRuntime();
|
|
3786
|
+
if (runtime.kind !== 'STABLE') {
|
|
3787
|
+
return { adapter: adapter.id, action: 'unsupported', line: serviceRuntimeLine(runtime) };
|
|
3788
|
+
}
|
|
3789
|
+
const plan = await planPersistentRuntime(adapter, serviceCommand(serviceInterval(values), runtime)).catch(() => null);
|
|
3513
3790
|
if (!plan)
|
|
3514
3791
|
return null;
|
|
3515
3792
|
return { adapter: adapter.id, action: plan.action, line: persistentRuntimeLine(adapter.id, plan) };
|
|
@@ -3564,15 +3841,42 @@ function serviceAdapter() {
|
|
|
3564
3841
|
* 지금 도는 실행 파일과 진입점을 그대로 쓴다 — 어느 build 를 쓸지는 그 진입점이 다시
|
|
3565
3842
|
* 정하므로(C-14), 등록물이 build 를 고르는 일은 없다.
|
|
3566
3843
|
*/
|
|
3567
|
-
function serviceCommand(intervalSeconds) {
|
|
3844
|
+
function serviceCommand(intervalSeconds, runtime) {
|
|
3568
3845
|
return {
|
|
3569
|
-
program:
|
|
3570
|
-
args: [
|
|
3846
|
+
program: runtime.node,
|
|
3847
|
+
args: [runtime.entry, 'runtime', 'tick', '--all'],
|
|
3571
3848
|
intervalSeconds,
|
|
3572
|
-
...serviceEnvironment(),
|
|
3849
|
+
...serviceEnvironment(runtime.node),
|
|
3573
3850
|
logPath: join(ascHome(), 'service.log'),
|
|
3574
3851
|
};
|
|
3575
3852
|
}
|
|
3853
|
+
/**
|
|
3854
|
+
* 등록물이 가리킬 Node 와 진입점 (P0 — fresh onboarding).
|
|
3855
|
+
*
|
|
3856
|
+
* 지금 이 프로세스가 어디서 도는지와 **다른 질문이다.** bootstrap 은 npx 캐시에서 돌 수
|
|
3857
|
+
* 있고, 그 자리는 지워진다. 전역 설치본이 있으면 그것이 답이고, 없으면 등록하지 않는다.
|
|
3858
|
+
*/
|
|
3859
|
+
async function serviceRuntime() {
|
|
3860
|
+
const stable = await globalRuntimeEntry();
|
|
3861
|
+
const check = await checkNodeRuntime(nodeRuntimeDeps());
|
|
3862
|
+
return resolveServiceRuntime({
|
|
3863
|
+
runningEntry: fileURLToPath(import.meta.url),
|
|
3864
|
+
runningNode: process.execPath,
|
|
3865
|
+
runningNodeVersion: process.version,
|
|
3866
|
+
...(stable ? { stableEntry: stable } : {}),
|
|
3867
|
+
...(check.ok ? {} : { nodeCandidates: check.candidates }),
|
|
3868
|
+
});
|
|
3869
|
+
}
|
|
3870
|
+
/** 전역 설치본의 진입점. npm 이 말하는 prefix 를 쓴다 — 경로를 지어내지 않는다. */
|
|
3871
|
+
async function globalRuntimeEntry() {
|
|
3872
|
+
const prefix = await execText('npm', ['prefix', '-g']);
|
|
3873
|
+
if (!prefix)
|
|
3874
|
+
return undefined;
|
|
3875
|
+
const entry = process.platform === 'win32'
|
|
3876
|
+
? join(prefix, 'node_modules', RUNTIME_PACKAGE, 'dist', 'cli', 'asc.js')
|
|
3877
|
+
: join(prefix, 'lib', 'node_modules', RUNTIME_PACKAGE, 'dist', 'cli', 'asc.js');
|
|
3878
|
+
return existsSync(entry) ? entry : undefined;
|
|
3879
|
+
}
|
|
3576
3880
|
/**
|
|
3577
3881
|
* 서비스가 외부 통로를 열 수 있게 하는 환경 (P0-R1).
|
|
3578
3882
|
*
|
|
@@ -3580,10 +3884,10 @@ function serviceCommand(intervalSeconds) {
|
|
|
3580
3884
|
* node 의 디렉터리를 맨 앞에 둬서 같은 node 의 npx 가 먼저 잡히게 한다. Windows 의 예약
|
|
3581
3885
|
* 작업은 사용자 환경을 그대로 물려받으므로 환경을 싣지 않는다.
|
|
3582
3886
|
*/
|
|
3583
|
-
function serviceEnvironment() {
|
|
3887
|
+
function serviceEnvironment(node) {
|
|
3584
3888
|
if (process.platform === 'win32')
|
|
3585
3889
|
return {};
|
|
3586
|
-
const tools = [
|
|
3890
|
+
const tools = [node, jamLauncher().command, 'glab', 'git'];
|
|
3587
3891
|
const { path } = servicePath(tools);
|
|
3588
3892
|
return { environment: { PATH: path } };
|
|
3589
3893
|
}
|
|
@@ -3605,7 +3909,17 @@ async function runRuntimeService(command, values) {
|
|
|
3605
3909
|
// 못 하는 것을 "했다"로 적지 않는다. 다만 이것이 오류는 아니다.
|
|
3606
3910
|
return 0;
|
|
3607
3911
|
}
|
|
3608
|
-
const
|
|
3912
|
+
const runtime = await serviceRuntime();
|
|
3913
|
+
if (runtime.kind !== 'STABLE') {
|
|
3914
|
+
// 등록물이 가리킬 안정된 자리가 없다. 무엇이 없어서인지 말한다 — 등록하지 않는 것이
|
|
3915
|
+
// 답이고, 깨진 등록을 남기는 것은 답이 아니다.
|
|
3916
|
+
if (values.json)
|
|
3917
|
+
console.log(JSON.stringify({ supported: true, stable: false, ...runtime }, null, 2));
|
|
3918
|
+
else
|
|
3919
|
+
console.error(serviceRuntimeLine(runtime));
|
|
3920
|
+
return command === 'status' || command === undefined ? 0 : 1;
|
|
3921
|
+
}
|
|
3922
|
+
const wanted = serviceCommand(serviceInterval(values), runtime);
|
|
3609
3923
|
if (command === undefined || command === 'status') {
|
|
3610
3924
|
const plan = await planPersistentRuntime(adapter, wanted);
|
|
3611
3925
|
if (values.json) {
|
|
@@ -53,6 +53,23 @@ export declare function closeToolClients(): Promise<void>;
|
|
|
53
53
|
* 후보가 갈리면 만들지 않는다 — `AMBIGUOUS_BINDING`은 사람이 정할 문제이고, 여기서 하나를
|
|
54
54
|
* 고르면 그 선택을 아무도 보지 못한다 (C-09 §4.2).
|
|
55
55
|
*/
|
|
56
|
+
/**
|
|
57
|
+
* 작업 항목을 읽는 통로의 역할 이름 (P0 F7).
|
|
58
|
+
*
|
|
59
|
+
* `rolesFor` 는 **선언된 binding 이 하나일 때만** 역할을 정한다. 그래서 code binding 과
|
|
60
|
+
* work binding 이 둘 다 `context.resource` 를 제공하면 아무 역할도 서지 않고, 작업 항목
|
|
61
|
+
* 조회는 "통로가 없다" 로 끝난다 — 실기계에서 감시는 되는데 `proceed --work` 만 실패한
|
|
62
|
+
* 자리가 여기다. 없는 것이 아니라 **누구에게 물을지 정하지 않은 것**이었다.
|
|
63
|
+
*
|
|
64
|
+
* 가르는 기준은 provider 이름이 아니라 capability 모양이다: 작업 항목 통로는 목록과 자원을
|
|
65
|
+
* 알고 **변경을 모른다**. 코드 통로는 변경을 안다. 그 차이가 곧 두 축의 정의다
|
|
66
|
+
* (C-09 §2.1 — Port 를 좁게 나눈 이유가 그것이다).
|
|
67
|
+
*/
|
|
68
|
+
export declare function workItemRoles(plan: BindingPlan, declared: readonly {
|
|
69
|
+
role: string;
|
|
70
|
+
adapter: string;
|
|
71
|
+
resource: string;
|
|
72
|
+
}[]): Partial<Record<Capability, string>>;
|
|
56
73
|
/**
|
|
57
74
|
* Profile이 선언한 역할 배정을 capability별 역할로 옮긴다 (C-09 §3.1·§4).
|
|
58
75
|
*
|
|
@@ -124,6 +124,32 @@ const PORT_OF = {
|
|
|
124
124
|
* 후보가 갈리면 만들지 않는다 — `AMBIGUOUS_BINDING`은 사람이 정할 문제이고, 여기서 하나를
|
|
125
125
|
* 고르면 그 선택을 아무도 보지 못한다 (C-09 §4.2).
|
|
126
126
|
*/
|
|
127
|
+
/**
|
|
128
|
+
* 작업 항목을 읽는 통로의 역할 이름 (P0 F7).
|
|
129
|
+
*
|
|
130
|
+
* `rolesFor` 는 **선언된 binding 이 하나일 때만** 역할을 정한다. 그래서 code binding 과
|
|
131
|
+
* work binding 이 둘 다 `context.resource` 를 제공하면 아무 역할도 서지 않고, 작업 항목
|
|
132
|
+
* 조회는 "통로가 없다" 로 끝난다 — 실기계에서 감시는 되는데 `proceed --work` 만 실패한
|
|
133
|
+
* 자리가 여기다. 없는 것이 아니라 **누구에게 물을지 정하지 않은 것**이었다.
|
|
134
|
+
*
|
|
135
|
+
* 가르는 기준은 provider 이름이 아니라 capability 모양이다: 작업 항목 통로는 목록과 자원을
|
|
136
|
+
* 알고 **변경을 모른다**. 코드 통로는 변경을 안다. 그 차이가 곧 두 축의 정의다
|
|
137
|
+
* (C-09 §2.1 — Port 를 좁게 나눈 이유가 그것이다).
|
|
138
|
+
*/
|
|
139
|
+
export function workItemRoles(plan, declared) {
|
|
140
|
+
const candidates = plan.bindings
|
|
141
|
+
.filter((binding) => binding.role !== undefined)
|
|
142
|
+
.filter((binding) => declared.some((d) => d.adapter === binding.adapterId && d.resource === binding.resource))
|
|
143
|
+
.filter((binding) => binding.provides.includes('context.resource') &&
|
|
144
|
+
binding.provides.includes('inventory.enumerate') &&
|
|
145
|
+
!binding.provides.includes('context.change'));
|
|
146
|
+
const roles = new Set(candidates.map((binding) => binding.role));
|
|
147
|
+
// 갈리면 고르지 않는다 — 그것은 사람이 정할 문제다 (C-09 §4.2).
|
|
148
|
+
if (roles.size !== 1)
|
|
149
|
+
return {};
|
|
150
|
+
const role = [...roles][0];
|
|
151
|
+
return { 'context.resource': role, 'inventory.enumerate': role, 'context.thread': role, 'context.history': role };
|
|
152
|
+
}
|
|
127
153
|
/**
|
|
128
154
|
* Profile이 선언한 역할 배정을 capability별 역할로 옮긴다 (C-09 §3.1·§4).
|
|
129
155
|
*
|
|
@@ -61,6 +61,46 @@ export type SetupState = {
|
|
|
61
61
|
* 사람에게 "그 도구를 설정할까요?"라고 되물으면, 사람은 자기가 이미 적어 둔 것을 다시
|
|
62
62
|
* 답하게 된다. 고칠 수 있는 것은 고치고, 사람만 할 수 있는 것에서만 멈춘다.
|
|
63
63
|
*/
|
|
64
|
+
/**
|
|
65
|
+
* 이 저장소 자신의 remote 에서 읽히는 Profile id (P0 fresh onboarding).
|
|
66
|
+
*
|
|
67
|
+
* **다른 논리 workspace 를 추측해 합치는 것이 아니다** (C-11 금지사항). 이 저장소가
|
|
68
|
+
* 스스로 증명하는 신원 하나이고, 그 이름의 Profile 이 이미 있으면 그것을 쓰고 없으면
|
|
69
|
+
* 만든다. 이것이 없어서 fresh 설치가 `ASC_PROFILE_SELECTION_REQUIRED` 앞에 멈췄고,
|
|
70
|
+
* 사람이 `profile adopt` 를 따로 쳐야 했다.
|
|
71
|
+
*/
|
|
72
|
+
adoptable?: {
|
|
73
|
+
id: string;
|
|
74
|
+
exists: boolean;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* 이 workspace 에 승인 권한자가 서 있는가, 그리고 지금 이 사람을 무엇으로 알아볼 수
|
|
78
|
+
* 있는가. `actor` 는 인증된 provider 가 말한 값이며 ASC 가 지어내지 않는다.
|
|
79
|
+
*/
|
|
80
|
+
identity?: {
|
|
81
|
+
wired: boolean;
|
|
82
|
+
actor?: string;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* 발견이 **갈리지 않고** 제안하는 결합. Profile 에 결합 선언이 없을 때만 본다.
|
|
86
|
+
* 갈리면 비어 있다 — 고르는 것은 사람이다 (C-09 §4.2).
|
|
87
|
+
*/
|
|
88
|
+
bindingProposal?: readonly {
|
|
89
|
+
role: string;
|
|
90
|
+
adapter: string;
|
|
91
|
+
resource: string;
|
|
92
|
+
}[];
|
|
93
|
+
/**
|
|
94
|
+
* 이 checkout 이 증명하는 정본 갈래. remote 가 스스로 말한 기본 branch 하나이며,
|
|
95
|
+
* Profile 에 정본 선언이 비어 있을 때만 관측한다. 알 수 없으면 없다 — 지어내면 세션이
|
|
96
|
+
* 읽지 못하는 정본을 딛고 서게 된다.
|
|
97
|
+
*/
|
|
98
|
+
canonicalProposal?: {
|
|
99
|
+
id: string;
|
|
100
|
+
provider: string;
|
|
101
|
+
remote: string;
|
|
102
|
+
ref: string;
|
|
103
|
+
};
|
|
64
104
|
workBinding?: {
|
|
65
105
|
adapter: string;
|
|
66
106
|
resource: string;
|
|
@@ -86,12 +126,63 @@ export type SetupChange =
|
|
|
86
126
|
strategy: 'npm-global';
|
|
87
127
|
from: string;
|
|
88
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* 이 저장소를 설명하는 Profile 을 만든다 (P0 fresh onboarding).
|
|
131
|
+
*
|
|
132
|
+
* 근거는 이 저장소의 remote 하나뿐이다. 다른 workspace 를 추측하지 않는다.
|
|
133
|
+
*/
|
|
134
|
+
| {
|
|
135
|
+
target: 'adopt-profile';
|
|
136
|
+
profile: string;
|
|
137
|
+
}
|
|
89
138
|
/** 이 checkout에 runtime을 붙인다. local scope면 저장소에는 아무것도 만들지 않는다. */
|
|
90
139
|
| {
|
|
91
140
|
target: 'attach-workspace';
|
|
92
141
|
scope: 'local' | 'project';
|
|
93
142
|
profile: string;
|
|
94
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* 지금 이 사람을 이 workspace 의 승인 권한자로 세운다.
|
|
146
|
+
*
|
|
147
|
+
* **비밀은 다루지 않는다** — 이름과 채널뿐이다. 이것이 계획에 없으면 fresh 설치가
|
|
148
|
+
* 승인자 0 인 채로 READY 가 되고, 사람이 `setup identity` 를 따로 쳐야 했다.
|
|
149
|
+
*/
|
|
150
|
+
| {
|
|
151
|
+
target: 'identity';
|
|
152
|
+
actor: string;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 발견이 증명한 결합을 Profile 에 적는다.
|
|
156
|
+
*
|
|
157
|
+
* 갈리는 것은 담지 않는다. 여기 없으면 사람이 Profile 을 손으로 고쳐야 했고, 그것이
|
|
158
|
+
* fresh onboarding 에서 실제로 필요했던 단계다.
|
|
159
|
+
*/
|
|
160
|
+
| {
|
|
161
|
+
target: 'profile-bindings';
|
|
162
|
+
profile: string;
|
|
163
|
+
bindings: readonly {
|
|
164
|
+
role: string;
|
|
165
|
+
adapter: string;
|
|
166
|
+
resource: string;
|
|
167
|
+
}[];
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Profile 에 정본 갈래를 적는다.
|
|
171
|
+
*
|
|
172
|
+
* 이것이 비어 있으면 판정이 전부 "확인 못 함" 으로 나온다 — relevance 도
|
|
173
|
+
* responsibility 도 정본을 딛고 서기 때문이다. 근거는 remote 가 스스로 말한 기본
|
|
174
|
+
* branch 이고, 그 값을 얻지 못하면 이 변경은 계획에 들지 않는다.
|
|
175
|
+
*/
|
|
176
|
+
| {
|
|
177
|
+
target: 'profile-canonical';
|
|
178
|
+
profile: string;
|
|
179
|
+
source: {
|
|
180
|
+
id: string;
|
|
181
|
+
provider: string;
|
|
182
|
+
remote: string;
|
|
183
|
+
ref: string;
|
|
184
|
+
};
|
|
185
|
+
}
|
|
95
186
|
/** Host 설치물을 지금 source에 맞춘다. 왜 필요한지까지 든다. */
|
|
96
187
|
| {
|
|
97
188
|
target: 'host-install';
|
|
@@ -190,17 +281,27 @@ export type SetupEffects = {
|
|
|
190
281
|
registerPersistentRuntime?(change: Extract<SetupChange, {
|
|
191
282
|
target: 'persistent-runtime';
|
|
192
283
|
}>): Promise<void>;
|
|
284
|
+
/** 이 저장소를 설명하는 Profile 을 만든다. */
|
|
285
|
+
adoptProfile?(change: Extract<SetupChange, {
|
|
286
|
+
target: 'adopt-profile';
|
|
287
|
+
}>): Promise<void>;
|
|
288
|
+
/** 승인 권한자를 세운다. 비밀은 다루지 않는다. */
|
|
289
|
+
wireIdentity?(change: Extract<SetupChange, {
|
|
290
|
+
target: 'identity';
|
|
291
|
+
}>): Promise<void>;
|
|
292
|
+
/** 발견이 증명한 결합을 Profile 에 적는다. */
|
|
293
|
+
declareBindings?(change: Extract<SetupChange, {
|
|
294
|
+
target: 'profile-bindings';
|
|
295
|
+
}>): Promise<void>;
|
|
296
|
+
/** remote 가 말한 정본 갈래를 Profile 에 적는다. */
|
|
297
|
+
declareCanonical?(change: Extract<SetupChange, {
|
|
298
|
+
target: 'profile-canonical';
|
|
299
|
+
}>): Promise<void>;
|
|
193
300
|
};
|
|
194
301
|
export type ApplyResult = {
|
|
195
302
|
applied: SetupChange[];
|
|
196
303
|
changesApplied: boolean;
|
|
197
304
|
};
|
|
198
|
-
/**
|
|
199
|
-
* plan에 적힌 것만 실행한다. **다시 판단하지 않는다** (C-14 불변식 ⑩).
|
|
200
|
-
*
|
|
201
|
-
* 여기서 상태를 다시 보고 마음을 바꾸면, 사람이 승인한 plan과 실제로 일어난 일이
|
|
202
|
-
* 달라진다. 그 순간 plan은 아무것도 보장하지 않는 문서가 된다.
|
|
203
|
-
*/
|
|
204
305
|
export declare function applySetupPlan(plan: SetupPlan, effects: SetupEffects): Promise<ApplyResult>;
|
|
205
306
|
/** 사람이 읽는 줄. 같은 plan에서 나온다 — agent가 보는 JSON과 다른 판단이 아니다. */
|
|
206
307
|
export declare function renderSetupPlan(plan: SetupPlan): string[];
|
|
@@ -66,7 +66,7 @@ export function computeSetupPlan(state) {
|
|
|
66
66
|
code: 'ASC_HOST_INSTALL_MODIFIED',
|
|
67
67
|
// 사람이 고친 것을 덮는 것은 사람이 정한다 — plan에 담아 몰래 적용하지 않는다.
|
|
68
68
|
// 다만 runtime 설치처럼 이 결정과 무관한 준비는 계획에 남는다.
|
|
69
|
-
changes,
|
|
69
|
+
changes: orderChanges(changes, false),
|
|
70
70
|
requiresUserAction: true,
|
|
71
71
|
...actions(mode, evidence, [
|
|
72
72
|
{ type: 'force_host_install', ...command(['host', host.id, 'install', '--force']) },
|
|
@@ -96,7 +96,7 @@ export function computeSetupPlan(state) {
|
|
|
96
96
|
return {
|
|
97
97
|
status: 'user_action_required',
|
|
98
98
|
code: 'ASC_WORK_BINDING_NEEDS_USER',
|
|
99
|
-
changes,
|
|
99
|
+
changes: orderChanges(changes, false),
|
|
100
100
|
requiresUserAction: true,
|
|
101
101
|
...actions(mode, evidence, [{ type: 'proceed', ...command(['setup', 'status']) }]),
|
|
102
102
|
};
|
|
@@ -112,6 +112,7 @@ export function computeSetupPlan(state) {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
if (state.ascRoot && !state.attachmentBroken) {
|
|
115
|
+
pushWorkspaceChanges(changes, state, evidence);
|
|
115
116
|
// 붙어 있어도 **무엇을 고를 수 있었는지**는 사실이다. 사용자 소유 Profile을 새로 놓고
|
|
116
117
|
// 계획을 물었을 때 그것이 어디에도 안 보이면, 놓은 사람은 경로를 의심하게 된다.
|
|
117
118
|
if (state.profileCandidates.length > 0) {
|
|
@@ -121,13 +122,23 @@ export function computeSetupPlan(state) {
|
|
|
121
122
|
}
|
|
122
123
|
// 아직 안 붙었거나, 붙이다 말았다(BROKEN). 무엇으로 붙을지는 사람이 정한다 —
|
|
123
124
|
// BROKEN이면 같은 선택으로 다시 붙이는 것이 repair다.
|
|
124
|
-
|
|
125
|
+
// 이 저장소가 스스로 증명하는 Profile 이 있으면 그것이 답이다 — 이름이 이미 있으면
|
|
126
|
+
// 그것을 쓰고, 없으면 만든다. 둘 다 "다른 workspace 를 추측"하는 것이 아니다.
|
|
127
|
+
const adoptable = state.adoptable;
|
|
128
|
+
const profile = state.requestedProfile ??
|
|
129
|
+
(adoptable && state.profileCandidates.includes(adoptable.id) ? adoptable.id : undefined) ??
|
|
130
|
+
(adoptable ? adoptable.id : undefined) ??
|
|
131
|
+
soleCandidate(state.profileCandidates);
|
|
132
|
+
if (adoptable && !state.requestedProfile && !adoptable.exists) {
|
|
133
|
+
evidence.push(`profile=${adoptable.id} (adopted from this repository)`);
|
|
134
|
+
changes.push({ target: 'adopt-profile', profile: adoptable.id });
|
|
135
|
+
}
|
|
125
136
|
if (!profile) {
|
|
126
137
|
evidence.push(`profile candidates=${state.profileCandidates.join(', ') || '(none)'}`);
|
|
127
138
|
return {
|
|
128
139
|
status: 'user_action_required',
|
|
129
140
|
code: 'ASC_PROFILE_SELECTION_REQUIRED',
|
|
130
|
-
changes,
|
|
141
|
+
changes: orderChanges(changes, false),
|
|
131
142
|
requiresUserAction: true,
|
|
132
143
|
profiles: state.profileCandidates,
|
|
133
144
|
// 고를 것이 **없을** 수도 있다 — 배포본이 들고 있는 것은 예시뿐이고, 이 프로젝트를
|
|
@@ -153,9 +164,36 @@ export function computeSetupPlan(state) {
|
|
|
153
164
|
evidence.push('adoption=project (explicit)');
|
|
154
165
|
}
|
|
155
166
|
changes.push({ target: 'attach-workspace', scope: state.scope, profile });
|
|
156
|
-
|
|
167
|
+
if (!changes.some((c) => c.target === 'adopt-profile')) {
|
|
168
|
+
evidence.push(`profile=${profile}${state.requestedProfile ? ' (given)' : ' (sole candidate)'}`);
|
|
169
|
+
}
|
|
170
|
+
pushWorkspaceChanges(changes, state, evidence, profile);
|
|
157
171
|
return finish(changes, evidence, state, mode, command);
|
|
158
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* workspace 가 서고 나서 필요한 것들 — 승인자와 결합 선언.
|
|
175
|
+
*
|
|
176
|
+
* 둘 다 **관측된 사실이 있을 때만** 계획에 든다. 승인자는 인증된 provider 가 말한 이름이고,
|
|
177
|
+
* 결합은 발견이 갈리지 않고 제안한 것이다. 없으면 담지 않는다 — 지어내면 그 값이 곧
|
|
178
|
+
* 사람이 겪는 오작동이 된다.
|
|
179
|
+
*/
|
|
180
|
+
function pushWorkspaceChanges(changes, state, evidence, attachingProfile) {
|
|
181
|
+
if (state.identity && !state.identity.wired && state.identity.actor) {
|
|
182
|
+
evidence.push(`identity=${state.identity.actor} (not yet recorded)`);
|
|
183
|
+
changes.push({ target: 'identity', actor: state.identity.actor });
|
|
184
|
+
}
|
|
185
|
+
const profile = attachingProfile ?? state.attachedProfile;
|
|
186
|
+
if (!profile)
|
|
187
|
+
return;
|
|
188
|
+
if (state.bindingProposal && state.bindingProposal.length > 0) {
|
|
189
|
+
evidence.push(`bindings proposed=${state.bindingProposal.map((b) => `${b.role}:${b.adapter}`).join(', ')}`);
|
|
190
|
+
changes.push({ target: 'profile-bindings', profile, bindings: state.bindingProposal });
|
|
191
|
+
}
|
|
192
|
+
if (state.canonicalProposal) {
|
|
193
|
+
evidence.push(`canonical proposed=${state.canonicalProposal.remote}/${state.canonicalProposal.ref}`);
|
|
194
|
+
changes.push({ target: 'profile-canonical', profile, source: state.canonicalProposal });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
159
197
|
/** 후보가 하나뿐이어도 대신 고르지 않는다 — 여기서 돌려주는 것은 "고를 것이 없다"뿐이다. */
|
|
160
198
|
function soleCandidate(candidates) {
|
|
161
199
|
return candidates.length === 1 ? candidates[0] : undefined;
|
|
@@ -172,7 +210,9 @@ function actions(mode, evidence, list) {
|
|
|
172
210
|
evidence,
|
|
173
211
|
};
|
|
174
212
|
}
|
|
175
|
-
function finish(
|
|
213
|
+
function finish(raw, evidence, state, mode, command) {
|
|
214
|
+
const settled = (Boolean(state.ascRoot) && !state.attachmentBroken) || raw.some((c) => c.target === 'attach-workspace');
|
|
215
|
+
const changes = orderChanges(raw, settled);
|
|
176
216
|
if (changes.length === 0) {
|
|
177
217
|
return {
|
|
178
218
|
status: 'already_configured',
|
|
@@ -194,6 +234,34 @@ function finish(changes, evidence, state, mode, command) {
|
|
|
194
234
|
* 여기서 상태를 다시 보고 마음을 바꾸면, 사람이 승인한 plan과 실제로 일어난 일이
|
|
195
235
|
* 달라진다. 그 순간 plan은 아무것도 보장하지 않는 문서가 된다.
|
|
196
236
|
*/
|
|
237
|
+
/**
|
|
238
|
+
* 실행 순서 (설계 §9 — fresh onboarding).
|
|
239
|
+
*
|
|
240
|
+
* **등록이 맨 마지막이다.** 예전에는 등록이 붙이기보다 먼저 계획에 들어가, OS 가 아직
|
|
241
|
+
* identity·binding 이 서지 않은 workspace 를 회차로 돌렸다. 그 회차는 실패했고, 사람은
|
|
242
|
+
* 자기가 setup 을 끝내기도 전에 실패 기록을 봤다.
|
|
243
|
+
*/
|
|
244
|
+
const CHANGE_ORDER = {
|
|
245
|
+
'runtime-install': 0,
|
|
246
|
+
'adopt-profile': 1,
|
|
247
|
+
'attach-workspace': 2,
|
|
248
|
+
identity: 3,
|
|
249
|
+
'profile-bindings': 4,
|
|
250
|
+
'profile-canonical': 5,
|
|
251
|
+
'work-binding-setup': 6,
|
|
252
|
+
'host-install': 7,
|
|
253
|
+
'persistent-runtime': 8,
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* 순서를 고정하고, **아직 설 자리가 아닌 등록은 뺀다.**
|
|
257
|
+
*
|
|
258
|
+
* 붙는 것이 이번 계획으로 끝나지 않으면 이 기계에 등록할 이유가 없다 — 등록물은
|
|
259
|
+
* workspace 를 돌보는 것이고, 돌볼 workspace 가 아직 없기 때문이다.
|
|
260
|
+
*/
|
|
261
|
+
function orderChanges(changes, attachmentSettled) {
|
|
262
|
+
const kept = attachmentSettled ? [...changes] : changes.filter((change) => change.target !== 'persistent-runtime');
|
|
263
|
+
return kept.sort((a, b) => CHANGE_ORDER[a.target] - CHANGE_ORDER[b.target]);
|
|
264
|
+
}
|
|
197
265
|
export async function applySetupPlan(plan, effects) {
|
|
198
266
|
const applied = [];
|
|
199
267
|
for (const change of plan.changes) {
|
|
@@ -220,6 +288,26 @@ export async function applySetupPlan(plan, effects) {
|
|
|
220
288
|
continue;
|
|
221
289
|
await effects.setupWorkBinding(change);
|
|
222
290
|
break;
|
|
291
|
+
case 'adopt-profile':
|
|
292
|
+
if (!effects.adoptProfile)
|
|
293
|
+
continue;
|
|
294
|
+
await effects.adoptProfile(change);
|
|
295
|
+
break;
|
|
296
|
+
case 'identity':
|
|
297
|
+
if (!effects.wireIdentity)
|
|
298
|
+
continue;
|
|
299
|
+
await effects.wireIdentity(change);
|
|
300
|
+
break;
|
|
301
|
+
case 'profile-bindings':
|
|
302
|
+
if (!effects.declareBindings)
|
|
303
|
+
continue;
|
|
304
|
+
await effects.declareBindings(change);
|
|
305
|
+
break;
|
|
306
|
+
case 'profile-canonical':
|
|
307
|
+
if (!effects.declareCanonical)
|
|
308
|
+
continue;
|
|
309
|
+
await effects.declareCanonical(change);
|
|
310
|
+
break;
|
|
223
311
|
}
|
|
224
312
|
applied.push(change);
|
|
225
313
|
}
|
|
@@ -254,5 +342,15 @@ function changeLine(change) {
|
|
|
254
342
|
return ` repair ${change.adapter} for ${change.resource} through its own setup (${change.version})`;
|
|
255
343
|
case 'persistent-runtime':
|
|
256
344
|
return ` register this machine's ASC runtime with ${change.adapter}`;
|
|
345
|
+
case 'adopt-profile':
|
|
346
|
+
return ` create profile ${change.profile} from this repository's remote`;
|
|
347
|
+
case 'identity':
|
|
348
|
+
return ` record ${change.actor} as this workspace's approver`;
|
|
349
|
+
case 'profile-bindings':
|
|
350
|
+
return ` declare bindings in ${change.profile}: ${change.bindings
|
|
351
|
+
.map((b) => `${b.role}=${b.adapter}:${b.resource}`)
|
|
352
|
+
.join(', ')}`;
|
|
353
|
+
case 'profile-canonical':
|
|
354
|
+
return ` declare canonical source in ${change.profile}: ${change.source.remote}/${change.source.ref}`;
|
|
257
355
|
}
|
|
258
356
|
}
|
|
@@ -8,7 +8,7 @@ export type AttachmentState = 'READY' | 'UNATTACHED' | 'BROKEN' | 'LOCK_DRIFT';
|
|
|
8
8
|
*/
|
|
9
9
|
export type GateState = 'OPEN' | 'BLOCKED' | 'DEGRADED';
|
|
10
10
|
export type SetupGate = {
|
|
11
|
-
id: 'approval' | 'monitor' | 'external-write';
|
|
11
|
+
id: 'approval' | 'monitor' | 'external-write' | 'canonical';
|
|
12
12
|
label: string;
|
|
13
13
|
state: GateState;
|
|
14
14
|
/** 무엇이 없어서 막혔는지. */
|
|
@@ -30,6 +30,8 @@ export type SetupStatus = {
|
|
|
30
30
|
gates: SetupGate[];
|
|
31
31
|
};
|
|
32
32
|
export type SetupInput = {
|
|
33
|
+
/** Profile 이 선언한 정본 갈래 수. 붙지 않았으면 알 수 없으므로 `undefined` 다. */
|
|
34
|
+
canonicalSources?: number;
|
|
33
35
|
attachment: AttachmentState;
|
|
34
36
|
/** 붙어 있는 Profile의 id와 출처. **Surface가 읽어 넘긴다** — Core는 경로를 모른다. */
|
|
35
37
|
profile?: {
|
|
@@ -28,7 +28,7 @@ export function assessSetup(input) {
|
|
|
28
28
|
attachment: input.attachment,
|
|
29
29
|
...(input.profile ? { profile: input.profile } : {}),
|
|
30
30
|
ready: [...ALWAYS_READY],
|
|
31
|
-
gates: [approvalGate(input), monitorGate(input), externalWriteGate(input)],
|
|
31
|
+
gates: [approvalGate(input), canonicalGate(input), monitorGate(input), externalWriteGate(input)],
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
/** 승인 결정. identities.json은 lock digest에 없어 재고정이 필요 없다. */
|
|
@@ -46,6 +46,30 @@ function approvalGate(input) {
|
|
|
46
46
|
howTo: ['open identities.json and add an approver in the $example form (no re-lock needed)'],
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* 정본 조회 (C-08 · C-11).
|
|
51
|
+
*
|
|
52
|
+
* 코드 결합은 선언돼 있는데 정본 갈래가 비어 있으면, 판정은 "확인 못 함" 으로만 나온다 —
|
|
53
|
+
* relevance 도 responsibility 도 정본을 딛고 서기 때문이다. 실기계에서 그 상태가 READY 로
|
|
54
|
+
* 보였고, 사람은 무엇이 빠졌는지 알 방법이 없었다. 없는 것을 없다고 말한다.
|
|
55
|
+
*/
|
|
56
|
+
function canonicalGate(input) {
|
|
57
|
+
// 붙지 않았으면 이 축을 판정하지 않는다 — 아직 Profile 이 없다.
|
|
58
|
+
if (input.attachment !== 'READY' || input.canonicalSources === undefined) {
|
|
59
|
+
return { id: 'canonical', label: 'canonical evidence', state: 'OPEN', missing: [], warnings: [], howTo: [] };
|
|
60
|
+
}
|
|
61
|
+
if (input.canonicalSources > 0) {
|
|
62
|
+
return { id: 'canonical', label: 'canonical evidence', state: 'OPEN', missing: [], warnings: [], howTo: [] };
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
id: 'canonical',
|
|
66
|
+
label: 'canonical evidence',
|
|
67
|
+
state: 'BLOCKED',
|
|
68
|
+
missing: ['the profile declares no canonical source, so nothing can be judged against a baseline'],
|
|
69
|
+
warnings: [],
|
|
70
|
+
howTo: [`declare one in the profile's canonical.sources — ${RESOLVE_AGAIN}`],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
49
73
|
/**
|
|
50
74
|
* 외부 감시. 두 설정이 서로 다른 것을 막는다 —
|
|
51
75
|
* controller.identities가 없으면 실행 자체가 안 되고(단 --as로 우회 가능),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export declare const RUNTIME_PACKAGE = "@asc-agent/runtime";
|
|
2
2
|
export declare const BOOTSTRAP_PACKAGE = "@asc-agent/bootstrap";
|
|
3
3
|
/** runtime과 bootstrap은 초기 release에서 lockstep이다. */
|
|
4
|
-
export declare const RELEASE_VERSION = "0.5.
|
|
5
|
-
export declare const RUNTIME_SPEC = "@asc-agent/runtime@0.5.
|
|
6
|
-
export declare const BOOTSTRAP_SPEC = "@asc-agent/bootstrap@0.5.
|
|
4
|
+
export declare const RELEASE_VERSION = "0.5.1";
|
|
5
|
+
export declare const RUNTIME_SPEC = "@asc-agent/runtime@0.5.1";
|
|
6
|
+
export declare const BOOTSTRAP_SPEC = "@asc-agent/bootstrap@0.5.1";
|
|
7
7
|
/**
|
|
8
8
|
* 아직 설치되지 않은 machine에서 그대로 실행되는 형태 (C-14 §3.4).
|
|
9
9
|
*
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export const RUNTIME_PACKAGE = '@asc-agent/runtime';
|
|
10
10
|
export const BOOTSTRAP_PACKAGE = '@asc-agent/bootstrap';
|
|
11
11
|
/** runtime과 bootstrap은 초기 release에서 lockstep이다. */
|
|
12
|
-
export const RELEASE_VERSION = '0.5.
|
|
12
|
+
export const RELEASE_VERSION = '0.5.1';
|
|
13
13
|
export const RUNTIME_SPEC = `${RUNTIME_PACKAGE}@${RELEASE_VERSION}`;
|
|
14
14
|
export const BOOTSTRAP_SPEC = `${BOOTSTRAP_PACKAGE}@${RELEASE_VERSION}`;
|
|
15
15
|
/**
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type NodeCandidate } from './node-runtime.ts';
|
|
2
|
+
/**
|
|
3
|
+
* 사라질 수 있는 자리인가.
|
|
4
|
+
*
|
|
5
|
+
* npx 캐시가 이 목록의 이유다. 임시 디렉터리도 같은 성질이고, 어느 쪽이든 **등록물이
|
|
6
|
+
* 가리키면 안 되는 자리**다. 목록은 짧게 유지한다 — 길어지면 그건 판정이 아니라 추측이다.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isTransientPath(path: string): boolean;
|
|
9
|
+
export type ServiceRuntimeInput = {
|
|
10
|
+
/** 지금 이 프로세스의 진입점 절대 경로. */
|
|
11
|
+
runningEntry: string;
|
|
12
|
+
/** 지금 이 프로세스의 Node 실행 파일. */
|
|
13
|
+
runningNode: string;
|
|
14
|
+
/** 지금 이 프로세스의 Node 버전 (`v24.1.0` 형태). */
|
|
15
|
+
runningNodeVersion: string;
|
|
16
|
+
/** 전역 설치본의 진입점. 있으면 이것이 먼저다 — 지워지지 않는 자리이기 때문이다. */
|
|
17
|
+
stableEntry?: string;
|
|
18
|
+
/** 이 기계에서 찾은 Node 후보들. 지금 Node 가 못 쓸 때만 본다. */
|
|
19
|
+
nodeCandidates?: readonly NodeCandidate[];
|
|
20
|
+
};
|
|
21
|
+
export type ServiceRuntimeResolution = {
|
|
22
|
+
kind: 'STABLE';
|
|
23
|
+
node: string;
|
|
24
|
+
entry: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'UNSTABLE';
|
|
27
|
+
/** 무엇이 없어서 못 하는가. 사람이 읽고 무엇을 할지 알 수 있어야 한다. */
|
|
28
|
+
reason: 'NO_STABLE_ENTRY' | 'NO_COMPATIBLE_NODE';
|
|
29
|
+
detail: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 등록물에 박을 Node 와 진입점을 정한다. **아무것도 실행하지 않는다** — 사실은 호출자가
|
|
33
|
+
* 관측해 넘긴다.
|
|
34
|
+
*
|
|
35
|
+
* 진입점: 전역 설치본이 있으면 그것. 없으면 지금 진입점이되 **임시 자리가 아닐 때만.**
|
|
36
|
+
* Node: 지금 Node 가 하한을 넘고 임시 자리가 아니면 그것. 아니면 후보 중 첫 번째.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveServiceRuntime(input: ServiceRuntimeInput): ServiceRuntimeResolution;
|
|
39
|
+
/** 사람이 읽는 한 줄. 왜 등록하지 않았는지가 여기 있어야 한다. */
|
|
40
|
+
export declare function serviceRuntimeLine(resolution: ServiceRuntimeResolution): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// 등록물이 무엇을 가리켜야 하는가 (C-12 · C-14 §3).
|
|
2
|
+
//
|
|
3
|
+
// 실기계에서 이런 일이 났다: `npx @asc-agent/bootstrap … setup apply` 로 처음 설치한
|
|
4
|
+
// 사람에게, 등록물이 **npx 캐시 안의 실행물**을 정본으로 박았다.
|
|
5
|
+
//
|
|
6
|
+
// ~/.npm/_npx/<hash>/node_modules/@asc-agent/runtime/dist/cli/asc.js
|
|
7
|
+
//
|
|
8
|
+
// npx 캐시는 지워지는 자리다. 그것을 지우면 등록물은 남고 실행물만 사라진다 — OS 는 계속
|
|
9
|
+
// 부르고 매번 실패한다. 등록은 **오래 남을 설치본**이 스스로 할 때만 뜻이 있다.
|
|
10
|
+
//
|
|
11
|
+
// 그래서 이 파일은 두 가지를 가른다:
|
|
12
|
+
//
|
|
13
|
+
// 지금 이 프로세스가 어디서 도는가 bootstrap 은 임시 자리에서 돌 수 있다
|
|
14
|
+
// 등록물이 무엇을 가리켜야 하는가 임시 자리는 절대 안 된다
|
|
15
|
+
//
|
|
16
|
+
// Node 도 같다. 등록물이 박는 Node 는 실제로 있고, 하한을 넘고, 임시 자리가 아니어야
|
|
17
|
+
// 한다 — 셋 중 하나라도 아니면 **등록하지 않는다.** 깨진 등록을 남기고 나중에 실패하는
|
|
18
|
+
// 것보다, 등록하지 않고 그 사실을 말하는 편이 낫다.
|
|
19
|
+
import { MINIMUM_NODE_MAJOR, majorOf } from "./node-runtime.js";
|
|
20
|
+
/**
|
|
21
|
+
* 사라질 수 있는 자리인가.
|
|
22
|
+
*
|
|
23
|
+
* npx 캐시가 이 목록의 이유다. 임시 디렉터리도 같은 성질이고, 어느 쪽이든 **등록물이
|
|
24
|
+
* 가리키면 안 되는 자리**다. 목록은 짧게 유지한다 — 길어지면 그건 판정이 아니라 추측이다.
|
|
25
|
+
*/
|
|
26
|
+
export function isTransientPath(path) {
|
|
27
|
+
const normalized = path.replace(/\\/g, '/');
|
|
28
|
+
return (normalized.includes('/_npx/') ||
|
|
29
|
+
normalized.includes('/_cacache/') ||
|
|
30
|
+
normalized.startsWith('/tmp/') ||
|
|
31
|
+
normalized.startsWith('/private/tmp/') ||
|
|
32
|
+
normalized.startsWith('/var/folders/') ||
|
|
33
|
+
/\/Temp\//i.test(normalized));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 등록물에 박을 Node 와 진입점을 정한다. **아무것도 실행하지 않는다** — 사실은 호출자가
|
|
37
|
+
* 관측해 넘긴다.
|
|
38
|
+
*
|
|
39
|
+
* 진입점: 전역 설치본이 있으면 그것. 없으면 지금 진입점이되 **임시 자리가 아닐 때만.**
|
|
40
|
+
* Node: 지금 Node 가 하한을 넘고 임시 자리가 아니면 그것. 아니면 후보 중 첫 번째.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveServiceRuntime(input) {
|
|
43
|
+
const entry = input.stableEntry && !isTransientPath(input.stableEntry)
|
|
44
|
+
? input.stableEntry
|
|
45
|
+
: !isTransientPath(input.runningEntry)
|
|
46
|
+
? input.runningEntry
|
|
47
|
+
: null;
|
|
48
|
+
if (!entry) {
|
|
49
|
+
return {
|
|
50
|
+
kind: 'UNSTABLE',
|
|
51
|
+
reason: 'NO_STABLE_ENTRY',
|
|
52
|
+
detail: `${input.runningEntry} is a temporary location — a registration pointing there breaks when it is cleared`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const usable = (path, version) => {
|
|
56
|
+
const major = majorOf(version);
|
|
57
|
+
return major !== null && major >= MINIMUM_NODE_MAJOR && !isTransientPath(path);
|
|
58
|
+
};
|
|
59
|
+
const node = usable(input.runningNode, input.runningNodeVersion)
|
|
60
|
+
? input.runningNode
|
|
61
|
+
: (input.nodeCandidates ?? []).find((candidate) => usable(candidate.path, candidate.version))?.path;
|
|
62
|
+
if (!node) {
|
|
63
|
+
return {
|
|
64
|
+
kind: 'UNSTABLE',
|
|
65
|
+
reason: 'NO_COMPATIBLE_NODE',
|
|
66
|
+
detail: `no Node ${MINIMUM_NODE_MAJOR} or newer at a stable path (this process runs ${input.runningNodeVersion} from ${input.runningNode})`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return { kind: 'STABLE', node, entry };
|
|
70
|
+
}
|
|
71
|
+
/** 사람이 읽는 한 줄. 왜 등록하지 않았는지가 여기 있어야 한다. */
|
|
72
|
+
export function serviceRuntimeLine(resolution) {
|
|
73
|
+
return resolution.kind === 'STABLE'
|
|
74
|
+
? `service runtime: ${resolution.node} ${resolution.entry}`
|
|
75
|
+
: `service runtime unavailable — ${resolution.detail}`;
|
|
76
|
+
}
|
|
@@ -493,6 +493,7 @@ export declare const UserOverride: z.ZodPipeline<z.ZodEffects<z.ZodUnknown, unkn
|
|
|
493
493
|
};
|
|
494
494
|
schemaVersion: 1;
|
|
495
495
|
monitorIdentities: string[];
|
|
496
|
+
identity: Record<string, string>;
|
|
496
497
|
approval: {
|
|
497
498
|
preferredChannel?: string | undefined;
|
|
498
499
|
messenger?: {
|
|
@@ -509,7 +510,6 @@ export declare const UserOverride: z.ZodPipeline<z.ZodEffects<z.ZodUnknown, unkn
|
|
|
509
510
|
lockedSettings?: string[] | undefined;
|
|
510
511
|
unionLists?: Record<string, string[]> | undefined;
|
|
511
512
|
};
|
|
512
|
-
identity: Record<string, string>;
|
|
513
513
|
$comment?: string | undefined;
|
|
514
514
|
}, {
|
|
515
515
|
schemaVersion: 1;
|
|
@@ -522,6 +522,7 @@ export declare const UserOverride: z.ZodPipeline<z.ZodEffects<z.ZodUnknown, unkn
|
|
|
522
522
|
} | undefined;
|
|
523
523
|
$comment?: string | undefined;
|
|
524
524
|
monitorIdentities?: string[] | undefined;
|
|
525
|
+
identity?: Record<string, string> | undefined;
|
|
525
526
|
approval?: {
|
|
526
527
|
preferredChannel?: string | undefined;
|
|
527
528
|
messenger?: {
|
|
@@ -538,7 +539,6 @@ export declare const UserOverride: z.ZodPipeline<z.ZodEffects<z.ZodUnknown, unkn
|
|
|
538
539
|
lockedSettings?: string[] | undefined;
|
|
539
540
|
unionLists?: Record<string, string[]> | undefined;
|
|
540
541
|
} | undefined;
|
|
541
|
-
identity?: Record<string, string> | undefined;
|
|
542
542
|
}>>;
|
|
543
543
|
export type UserOverride = z.infer<typeof UserOverride>;
|
|
544
544
|
/** attach 재현성 metadata (OM §4.9). Resolver만 쓴다. */
|