@athsra/cli 1.0.3 → 1.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/README.md +34 -10
- package/package.json +3 -3
- package/src/commands/delete.ts +16 -13
- package/src/commands/get.ts +8 -5
- package/src/commands/handoff.ts +13 -3
- package/src/commands/login.ts +164 -59
- package/src/commands/logout.ts +3 -2
- package/src/commands/ls.ts +32 -10
- package/src/commands/manifest.ts +2 -2
- package/src/commands/mcp.ts +259 -7
- package/src/commands/migrate-envelopes.ts +55 -3
- package/src/commands/purge.ts +13 -10
- package/src/commands/restore.ts +10 -6
- package/src/commands/rollback.ts +12 -9
- package/src/commands/rotate-master.ts +13 -13
- package/src/commands/run.ts +6 -24
- package/src/commands/service-token.ts +15 -31
- package/src/commands/set.ts +7 -6
- package/src/commands/unset.ts +11 -8
- package/src/commands/versions.ts +7 -5
- package/src/index.ts +12 -8
- package/src/lib/auth-context.ts +74 -12
- package/src/lib/auth-proof.ts +10 -0
- package/src/lib/auto-project.ts +58 -14
- package/src/lib/client.ts +94 -17
- package/src/lib/config.ts +2 -0
- package/src/lib/device-login.ts +157 -0
- package/src/lib/env-format.ts +1 -1
- package/src/lib/envelope.ts +105 -15
- package/src/lib/identity-key.ts +21 -0
- package/src/lib/keyring.ts +25 -0
- package/src/lib/mcp-register.ts +223 -0
- package/src/lib/mcp-tools/admin.ts +267 -0
- package/src/lib/mcp-tools/args.ts +26 -0
- package/src/lib/mcp-tools/confirm.ts +21 -0
- package/src/lib/mcp-tools/defs.ts +388 -3
- package/src/lib/mcp-tools/login.ts +156 -0
- package/src/lib/mcp-tools/mask.ts +41 -0
- package/src/lib/mcp-tools/read.ts +115 -1
- package/src/lib/mcp-tools/result.ts +5 -5
- package/src/lib/mcp-tools/run.ts +101 -0
- package/src/lib/mcp-tools/write.ts +84 -5
- package/src/lib/oidc-flow.ts +43 -1
- package/src/lib/org-rewrap.ts +9 -3
- package/src/lib/service-tokens.ts +62 -0
|
@@ -12,20 +12,58 @@ export interface ToolDef {
|
|
|
12
12
|
name: string;
|
|
13
13
|
description: string;
|
|
14
14
|
inputSchema: Record<string, unknown>;
|
|
15
|
-
annotations?: {
|
|
15
|
+
annotations?: {
|
|
16
|
+
destructiveHint?: boolean;
|
|
17
|
+
readOnlyHint?: boolean;
|
|
18
|
+
idempotentHint?: boolean;
|
|
19
|
+
openWorldHint?: boolean;
|
|
20
|
+
};
|
|
16
21
|
}
|
|
17
22
|
|
|
23
|
+
/**
|
|
24
|
+
* 환경(config) inputSchema property — secret 도구 공용. optional(required 미포함 = 하위호환).
|
|
25
|
+
* 핸들러는 `readString(args,'config') ?? 'default'` 로 소비. 값 검증은 worker(sanitizeConfig)가 단일원.
|
|
26
|
+
*/
|
|
27
|
+
const CONFIG_PROP = {
|
|
28
|
+
config: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Environment/config (e.g. dev/staging/prod). Optional — defaults to "default".',
|
|
31
|
+
},
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
18
34
|
export const READ_TOOLS: ToolDef[] = [
|
|
19
35
|
{
|
|
20
36
|
name: 'athsra_whoami',
|
|
21
37
|
description:
|
|
22
38
|
'Check athsra authentication status for THIS machine. Call this FIRST when onboarding athsra. ' +
|
|
23
39
|
'Returns { authenticated, userId, machineId } (identity only, NO secrets). If not authenticated, ' +
|
|
24
|
-
'
|
|
25
|
-
'
|
|
40
|
+
'call athsra_login_start to begin a browser login right from chat (no terminal needed) — ' +
|
|
41
|
+
'or run `athsra login` in a terminal. All other athsra tools require authentication first.',
|
|
26
42
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
27
43
|
annotations: { readOnlyHint: true },
|
|
28
44
|
},
|
|
45
|
+
{
|
|
46
|
+
name: 'athsra_login_start',
|
|
47
|
+
description:
|
|
48
|
+
'Begin a browser-based athsra login from chat — NO terminal needed. Returns a verification ' +
|
|
49
|
+
'URL, user_code and device key fingerprint to relay to the user. The user opens the URL, ' +
|
|
50
|
+
'checks the on-screen fingerprint matches (phishing guard), and approves with their master ' +
|
|
51
|
+
'password — which never leaves the browser. Then poll athsra_login_status. Calling start ' +
|
|
52
|
+
'while a flow is pending returns the SAME flow (idempotent). The device_code secret never ' +
|
|
53
|
+
'appears in any response.',
|
|
54
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
55
|
+
annotations: { readOnlyHint: false },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'athsra_login_status',
|
|
59
|
+
description:
|
|
60
|
+
'Poll the pending in-chat login (respect retry_after_seconds — too-fast calls return a ' +
|
|
61
|
+
'cached status without hitting the server). On approval the identity key + token are ' +
|
|
62
|
+
'stored in the OS keyring BEFORE this returns status:"approved" — afterwards every athsra ' +
|
|
63
|
+
'tool works. Terminal states: denied, expired (call athsra_login_start again).',
|
|
64
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
65
|
+
annotations: { readOnlyHint: false },
|
|
66
|
+
},
|
|
29
67
|
{
|
|
30
68
|
name: 'athsra_list_projects',
|
|
31
69
|
description:
|
|
@@ -52,6 +90,7 @@ export const READ_TOOLS: ToolDef[] = [
|
|
|
52
90
|
type: 'object',
|
|
53
91
|
properties: {
|
|
54
92
|
project: { type: 'string', minLength: 1, description: 'Envelope name.' },
|
|
93
|
+
...CONFIG_PROP,
|
|
55
94
|
},
|
|
56
95
|
required: ['project'],
|
|
57
96
|
additionalProperties: false,
|
|
@@ -97,6 +136,57 @@ export const READ_TOOLS: ToolDef[] = [
|
|
|
97
136
|
additionalProperties: false,
|
|
98
137
|
},
|
|
99
138
|
},
|
|
139
|
+
{
|
|
140
|
+
name: 'athsra_versions',
|
|
141
|
+
description:
|
|
142
|
+
'List the version history of a project envelope (version ids, timestamps, sizes) plus a ' +
|
|
143
|
+
'tombstone if soft-deleted. Metadata only — no secret values. Use to pick a rollback target.',
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: { project: { type: 'string' }, ...CONFIG_PROP },
|
|
147
|
+
required: ['project'],
|
|
148
|
+
additionalProperties: false,
|
|
149
|
+
},
|
|
150
|
+
annotations: { readOnlyHint: true },
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'athsra_audit',
|
|
154
|
+
description:
|
|
155
|
+
'Query the audit log (who did what, when) — actor/action/path/status metadata, never secret ' +
|
|
156
|
+
'values. Optional filters: action, actor, cursor (pagination). Returns entries + nextCursor.',
|
|
157
|
+
inputSchema: {
|
|
158
|
+
type: 'object',
|
|
159
|
+
properties: {
|
|
160
|
+
action: { type: 'string' },
|
|
161
|
+
actor: { type: 'string' },
|
|
162
|
+
cursor: { type: 'string' },
|
|
163
|
+
},
|
|
164
|
+
additionalProperties: false,
|
|
165
|
+
},
|
|
166
|
+
annotations: { readOnlyHint: true },
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'athsra_list_orgs',
|
|
170
|
+
description: 'List the orgs you belong to and which one is current (id, slug, role, status).',
|
|
171
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
172
|
+
annotations: { readOnlyHint: true },
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'athsra_org_info',
|
|
176
|
+
description:
|
|
177
|
+
'Show the current org with its members (identifier, role, status) plus your org list. ' +
|
|
178
|
+
'Identity/role metadata only — no secrets.',
|
|
179
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
180
|
+
annotations: { readOnlyHint: true },
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'athsra_doctor',
|
|
184
|
+
description:
|
|
185
|
+
'Self-diagnose auth/session health when other tools fail (authenticated? session valid?). ' +
|
|
186
|
+
'Returns checks + a healthy flag. Touches no secrets.',
|
|
187
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
188
|
+
annotations: { readOnlyHint: true },
|
|
189
|
+
},
|
|
100
190
|
];
|
|
101
191
|
|
|
102
192
|
export const WRITE_TOOLS: ToolDef[] = [
|
|
@@ -112,6 +202,7 @@ export const WRITE_TOOLS: ToolDef[] = [
|
|
|
112
202
|
project: { type: 'string', minLength: 1 },
|
|
113
203
|
key: { type: 'string', minLength: 1, pattern: '^[A-Za-z_][A-Za-z0-9_]*$' },
|
|
114
204
|
value: { type: 'string', description: 'Secret value (not logged, not echoed).' },
|
|
205
|
+
...CONFIG_PROP,
|
|
115
206
|
},
|
|
116
207
|
required: ['project', 'key', 'value'],
|
|
117
208
|
additionalProperties: false,
|
|
@@ -128,6 +219,7 @@ export const WRITE_TOOLS: ToolDef[] = [
|
|
|
128
219
|
properties: {
|
|
129
220
|
project: { type: 'string', minLength: 1 },
|
|
130
221
|
key: { type: 'string', minLength: 1 },
|
|
222
|
+
...CONFIG_PROP,
|
|
131
223
|
},
|
|
132
224
|
required: ['project', 'key'],
|
|
133
225
|
additionalProperties: false,
|
|
@@ -176,4 +268,297 @@ export const WRITE_TOOLS: ToolDef[] = [
|
|
|
176
268
|
},
|
|
177
269
|
annotations: { destructiveHint: true, readOnlyHint: false },
|
|
178
270
|
},
|
|
271
|
+
{
|
|
272
|
+
name: 'athsra_rollback',
|
|
273
|
+
description:
|
|
274
|
+
'Roll a project envelope back to a previous version (pick from athsra_versions). Requires ' +
|
|
275
|
+
'confirm=<project>. Creates a new version pointing at the old content.',
|
|
276
|
+
inputSchema: {
|
|
277
|
+
type: 'object',
|
|
278
|
+
properties: {
|
|
279
|
+
project: { type: 'string' },
|
|
280
|
+
version: { type: 'string', description: 'version_id from athsra_versions' },
|
|
281
|
+
confirm: { type: 'string', description: 'must equal the project name to proceed' },
|
|
282
|
+
...CONFIG_PROP,
|
|
283
|
+
},
|
|
284
|
+
required: ['project', 'version', 'confirm'],
|
|
285
|
+
additionalProperties: false,
|
|
286
|
+
},
|
|
287
|
+
annotations: { destructiveHint: true, readOnlyHint: false },
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: 'athsra_delete_project',
|
|
291
|
+
description:
|
|
292
|
+
'Soft-delete a project envelope (recoverable via athsra_restore_project). Requires ' +
|
|
293
|
+
'confirm=<project>. Hard delete is not exposed over MCP.',
|
|
294
|
+
inputSchema: {
|
|
295
|
+
type: 'object',
|
|
296
|
+
properties: {
|
|
297
|
+
project: { type: 'string' },
|
|
298
|
+
confirm: { type: 'string', description: 'must equal the project name to proceed' },
|
|
299
|
+
...CONFIG_PROP,
|
|
300
|
+
},
|
|
301
|
+
required: ['project', 'confirm'],
|
|
302
|
+
additionalProperties: false,
|
|
303
|
+
},
|
|
304
|
+
annotations: { destructiveHint: true, readOnlyHint: false },
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: 'athsra_restore_project',
|
|
308
|
+
description:
|
|
309
|
+
'Restore a soft-deleted project envelope to its last state. Non-destructive — no confirm needed.',
|
|
310
|
+
inputSchema: {
|
|
311
|
+
type: 'object',
|
|
312
|
+
properties: { project: { type: 'string' }, ...CONFIG_PROP },
|
|
313
|
+
required: ['project'],
|
|
314
|
+
additionalProperties: false,
|
|
315
|
+
},
|
|
316
|
+
annotations: { idempotentHint: true, readOnlyHint: false },
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: 'athsra_bulk_set',
|
|
320
|
+
description:
|
|
321
|
+
'Set multiple secrets in one read→merge→write (single version bump). `secrets` is an object ' +
|
|
322
|
+
'of KEY→value. Values are never echoed back (key names only in the response).',
|
|
323
|
+
inputSchema: {
|
|
324
|
+
type: 'object',
|
|
325
|
+
properties: {
|
|
326
|
+
project: { type: 'string' },
|
|
327
|
+
secrets: { type: 'object', additionalProperties: { type: 'string' } },
|
|
328
|
+
...CONFIG_PROP,
|
|
329
|
+
},
|
|
330
|
+
required: ['project', 'secrets'],
|
|
331
|
+
additionalProperties: false,
|
|
332
|
+
},
|
|
333
|
+
annotations: { destructiveHint: true, readOnlyHint: false },
|
|
334
|
+
},
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* value tier (1.2.0) — secret 평문이 응답 경계를 넘는 도구 (get_secret_value/run).
|
|
339
|
+
* `ATHSRA_MCP_READ_VALUES=1` opt-in 시에만 노출·dispatch. write(변경)·admin(계정수술)과
|
|
340
|
+
* 위험 축이 직교 — "값 읽기"만 별도 게이트. get_secret_value 는 기본 마스킹(prefix+length+
|
|
341
|
+
* sha256), full 평문은 opt-in + confirm=<project> 이중 게이트. run 은 값 미노출 주입 실행.
|
|
342
|
+
* athsra MCP 는 outward 도구가 없어 단독 trifecta 미형성 — 값을 본 호출측 agent 의 외부
|
|
343
|
+
* 전송은 그 agent 의 disallowedTools 책임 (description 경고 + governance private-pattern).
|
|
344
|
+
*/
|
|
345
|
+
export const VALUE_TOOLS: ToolDef[] = [
|
|
346
|
+
{
|
|
347
|
+
name: 'athsra_get_secret_value',
|
|
348
|
+
description:
|
|
349
|
+
'Reveal a secret value from an envelope. SAFE-BY-DEFAULT: returns a MASKED descriptor ' +
|
|
350
|
+
'(value prefix + length + sha256) unless full=true. full=true requires BOTH ' +
|
|
351
|
+
'ATHSRA_MCP_READ_VALUES=1 (server env) AND confirm=<project>. NOTE: athsra has no ' +
|
|
352
|
+
'outward/send tools — but the CALLING agent might. Never paste a revealed value into ' +
|
|
353
|
+
'another tool (Slack/Gmail/email/web). Prefer athsra_run to inject values into a process ' +
|
|
354
|
+
'WITHOUT seeing them.',
|
|
355
|
+
inputSchema: {
|
|
356
|
+
type: 'object',
|
|
357
|
+
properties: {
|
|
358
|
+
project: { type: 'string', minLength: 1, description: 'Envelope name.' },
|
|
359
|
+
key: { type: 'string', minLength: 1, description: 'Secret key to reveal.' },
|
|
360
|
+
full: {
|
|
361
|
+
type: 'boolean',
|
|
362
|
+
description:
|
|
363
|
+
'Reveal full plaintext (requires ATHSRA_MCP_READ_VALUES=1 + confirm). Default false = masked.',
|
|
364
|
+
},
|
|
365
|
+
confirm: { type: 'string', description: 'Must equal project name when full=true.' },
|
|
366
|
+
...CONFIG_PROP,
|
|
367
|
+
},
|
|
368
|
+
required: ['project', 'key'],
|
|
369
|
+
additionalProperties: false,
|
|
370
|
+
},
|
|
371
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: 'athsra_run',
|
|
375
|
+
description:
|
|
376
|
+
'Run a command with the envelope secrets injected as env vars — WITHOUT exposing values to ' +
|
|
377
|
+
'you. Returns exit code (and, only if return_output=true, captured output scrubbed of known ' +
|
|
378
|
+
'secret values). Known build/runtime commands (bun/node/npm/pnpm/yarn/wrangler/bunx/npx/' +
|
|
379
|
+
'vite/tsx) run directly; any other command requires confirm=<project> (deliberate-exec ' +
|
|
380
|
+
'gate). Mirrors `athsra run <project> -- <cmd>`. Secret values never appear in the response.',
|
|
381
|
+
inputSchema: {
|
|
382
|
+
type: 'object',
|
|
383
|
+
properties: {
|
|
384
|
+
project: { type: 'string', minLength: 1, description: 'Envelope name.' },
|
|
385
|
+
command: {
|
|
386
|
+
type: 'string',
|
|
387
|
+
minLength: 1,
|
|
388
|
+
description: 'Executable (e.g. "bun", "wrangler").',
|
|
389
|
+
},
|
|
390
|
+
args: { type: 'array', items: { type: 'string' }, description: 'Command arguments.' },
|
|
391
|
+
cwd: { type: 'string', description: 'Working directory (default: process cwd).' },
|
|
392
|
+
return_output: {
|
|
393
|
+
type: 'boolean',
|
|
394
|
+
description: 'Include scrubbed stdout/stderr in the response (default false).',
|
|
395
|
+
},
|
|
396
|
+
timeout_ms: {
|
|
397
|
+
type: 'integer',
|
|
398
|
+
minimum: 1000,
|
|
399
|
+
maximum: 600000,
|
|
400
|
+
description: 'Kill the child after N ms (default 120000).',
|
|
401
|
+
},
|
|
402
|
+
confirm: {
|
|
403
|
+
type: 'string',
|
|
404
|
+
description: 'Must equal project for non-allowlisted commands.',
|
|
405
|
+
},
|
|
406
|
+
...CONFIG_PROP,
|
|
407
|
+
},
|
|
408
|
+
required: ['project', 'command'],
|
|
409
|
+
additionalProperties: false,
|
|
410
|
+
},
|
|
411
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
412
|
+
},
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* admin tier (Phase 5 B1 골격 → B4 +7) — org/멤버/service-token/purge 고위험 도구.
|
|
417
|
+
* `ATHSRA_MCP_ADMIN=1` opt-in 시에만 노출·dispatch. destructive 는 confirm 정확일치
|
|
418
|
+
* (requireConfirm, auth 전 차단). master pw 소비 3종(remove_member/share/token_create)은
|
|
419
|
+
* identity 디바이스에서 actionable 거부. 시크릿 값 무노출 — 유일 예외 service_token_create
|
|
420
|
+
* 의 1회성 ats_* (warning 동반).
|
|
421
|
+
*/
|
|
422
|
+
export const ADMIN_TOOLS: ToolDef[] = [
|
|
423
|
+
{
|
|
424
|
+
name: 'athsra_org_invite',
|
|
425
|
+
description:
|
|
426
|
+
'Invite a member to the current org by identifier (email). Unknown identifiers create a ' +
|
|
427
|
+
'JIT pending user. role: member (default) or admin. The invitee accepts via `athsra login` ' +
|
|
428
|
+
'then `athsra org use`. Envelope access is separate — share per project with ' +
|
|
429
|
+
'athsra_project_share afterwards.',
|
|
430
|
+
inputSchema: {
|
|
431
|
+
type: 'object',
|
|
432
|
+
properties: {
|
|
433
|
+
identifier: {
|
|
434
|
+
type: 'string',
|
|
435
|
+
minLength: 1,
|
|
436
|
+
description: 'Member email (or numeric user id).',
|
|
437
|
+
},
|
|
438
|
+
role: { type: 'string', enum: ['member', 'admin'] },
|
|
439
|
+
},
|
|
440
|
+
required: ['identifier'],
|
|
441
|
+
additionalProperties: false,
|
|
442
|
+
},
|
|
443
|
+
annotations: { readOnlyHint: false },
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: 'athsra_org_remove_member',
|
|
447
|
+
description:
|
|
448
|
+
'Remove a member from the current org (their org tokens are revoked). Requires ' +
|
|
449
|
+
'confirm=<identifier> (exact match — deliberate-action gate). When called by the org ' +
|
|
450
|
+
'owner, DEKs of envelopes shared with the removed member are rotated (real cryptographic ' +
|
|
451
|
+
'revocation); non-owner callers get a follow-up instruction instead. Consumes the master ' +
|
|
452
|
+
'password — not available on identity-mode devices.',
|
|
453
|
+
inputSchema: {
|
|
454
|
+
type: 'object',
|
|
455
|
+
properties: {
|
|
456
|
+
identifier: {
|
|
457
|
+
type: 'string',
|
|
458
|
+
minLength: 1,
|
|
459
|
+
description: 'Member email or user id (athsra_org_info).',
|
|
460
|
+
},
|
|
461
|
+
confirm: { type: 'string', description: 'must equal identifier to proceed' },
|
|
462
|
+
},
|
|
463
|
+
required: ['identifier', 'confirm'],
|
|
464
|
+
additionalProperties: false,
|
|
465
|
+
},
|
|
466
|
+
annotations: { destructiveHint: true, readOnlyHint: false },
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: 'athsra_project_share',
|
|
470
|
+
description:
|
|
471
|
+
'Share ONE project with an org member: grants the worker ACL (read or write) and re-wraps ' +
|
|
472
|
+
'the envelope DEK to the member public key (E2EE member recipient — the server never sees ' +
|
|
473
|
+
'plaintext). The member must have provisioned an identity key via `athsra login` first. ' +
|
|
474
|
+
'Consumes the master password — not available on identity-mode devices.',
|
|
475
|
+
inputSchema: {
|
|
476
|
+
type: 'object',
|
|
477
|
+
properties: {
|
|
478
|
+
project: { type: 'string', minLength: 1 },
|
|
479
|
+
identifier: { type: 'string', minLength: 1, description: 'Member email or user id.' },
|
|
480
|
+
perms: { type: 'string', enum: ['read', 'write'], description: 'default read' },
|
|
481
|
+
},
|
|
482
|
+
required: ['project', 'identifier'],
|
|
483
|
+
additionalProperties: false,
|
|
484
|
+
},
|
|
485
|
+
annotations: { readOnlyHint: false },
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: 'athsra_project_unshare',
|
|
489
|
+
description:
|
|
490
|
+
"Revoke a member's ACL on one project — API access is blocked immediately. HONEST " +
|
|
491
|
+
'LIMITATION: the envelope DEK and member recipient are NOT rotated, so a member who ' +
|
|
492
|
+
'already cached the envelope can still decrypt past versions. For cryptographic ' +
|
|
493
|
+
'revocation the owner removes the member (athsra_org_remove_member), which rotates DEKs.',
|
|
494
|
+
inputSchema: {
|
|
495
|
+
type: 'object',
|
|
496
|
+
properties: {
|
|
497
|
+
project: { type: 'string', minLength: 1 },
|
|
498
|
+
identifier: { type: 'string', minLength: 1, description: 'Member email or user id.' },
|
|
499
|
+
},
|
|
500
|
+
required: ['project', 'identifier'],
|
|
501
|
+
additionalProperties: false,
|
|
502
|
+
},
|
|
503
|
+
annotations: { destructiveHint: true, readOnlyHint: false },
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: 'athsra_service_token_create',
|
|
507
|
+
description:
|
|
508
|
+
'Create a scoped service token (ats_*) for one project — headless hosts / CI decrypt the ' +
|
|
509
|
+
'envelope E2EE without the master password. expires_days is REQUIRED (1..365): ' +
|
|
510
|
+
'MCP-issued credentials always expire. THE ONLY athsra tool that returns a credential — ' +
|
|
511
|
+
'the token is shown exactly once; store it immediately in a secret store, never in code ' +
|
|
512
|
+
'or chat. Consumes the master password — not available on identity-mode devices.',
|
|
513
|
+
inputSchema: {
|
|
514
|
+
type: 'object',
|
|
515
|
+
properties: {
|
|
516
|
+
project: { type: 'string', minLength: 1 },
|
|
517
|
+
label: {
|
|
518
|
+
type: 'string',
|
|
519
|
+
minLength: 1,
|
|
520
|
+
description: 'Identifier for audit/rotation (e.g. "nas-docker").',
|
|
521
|
+
},
|
|
522
|
+
perms: { type: 'string', enum: ['read', 'write'], description: 'default read' },
|
|
523
|
+
expires_days: { type: 'integer', minimum: 1, maximum: 365 },
|
|
524
|
+
},
|
|
525
|
+
required: ['project', 'label', 'expires_days'],
|
|
526
|
+
additionalProperties: false,
|
|
527
|
+
},
|
|
528
|
+
annotations: { readOnlyHint: false },
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
name: 'athsra_service_token_revoke',
|
|
532
|
+
description:
|
|
533
|
+
'Revoke a service token (ats_*) immediately — worker auth rejects it at once (D1 strong ' +
|
|
534
|
+
'consistency). Use on leak suspicion or rotation. The stale envelope recipient entry left ' +
|
|
535
|
+
'behind is harmless (auth-by-hash already refuses).',
|
|
536
|
+
inputSchema: {
|
|
537
|
+
type: 'object',
|
|
538
|
+
properties: {
|
|
539
|
+
token: { type: 'string', minLength: 1, description: 'The ats_… token to revoke.' },
|
|
540
|
+
},
|
|
541
|
+
required: ['token'],
|
|
542
|
+
additionalProperties: false,
|
|
543
|
+
},
|
|
544
|
+
annotations: { destructiveHint: true, readOnlyHint: false },
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
name: 'athsra_purge',
|
|
548
|
+
description:
|
|
549
|
+
'PERMANENTLY hard-delete a project: every version, the current pointer and any tombstone ' +
|
|
550
|
+
'are removed — NOT recoverable, unlike athsra_delete_project (soft). Requires ' +
|
|
551
|
+
'confirm=<project> (exact match).',
|
|
552
|
+
inputSchema: {
|
|
553
|
+
type: 'object',
|
|
554
|
+
properties: {
|
|
555
|
+
project: { type: 'string', minLength: 1 },
|
|
556
|
+
confirm: { type: 'string', description: 'must equal the project name to proceed' },
|
|
557
|
+
...CONFIG_PROP,
|
|
558
|
+
},
|
|
559
|
+
required: ['project', 'confirm'],
|
|
560
|
+
additionalProperties: false,
|
|
561
|
+
},
|
|
562
|
+
annotations: { destructiveHint: true, readOnlyHint: false },
|
|
563
|
+
},
|
|
179
564
|
];
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-tools/login.ts — Phase 5 B5. MCP in-chat 로그인 (athsra_login_start / athsra_login_status).
|
|
3
|
+
*
|
|
4
|
+
* AI 가 터미널 없이 채팅 안에서 인증을 부트스트랩한다:
|
|
5
|
+
* start → user 에게 URL+user_code+fingerprint 전달 → 브라우저 승인(master pw 는 거기서만)
|
|
6
|
+
* → status poll → approved 시 keyring 저장 완료 후 반환 → 이후 전 도구 사용 가능.
|
|
7
|
+
*
|
|
8
|
+
* 보안:
|
|
9
|
+
* - device_code 는 모듈 메모리(activeFlow)에만 — 어떤 응답에도 미포함 (탈취=토큰 가로채기).
|
|
10
|
+
* - 단일 activeFlow — start 재호출은 진행 중 flow 를 재반환 (device code 남발 방지).
|
|
11
|
+
* - RFC 8628 interval 준수 — 과속 status 호출은 worker 미타격, 캐시 + retry_after 반환.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
completeIdentityLogin,
|
|
16
|
+
type IdentityFlow,
|
|
17
|
+
pollDeviceTokenOnce,
|
|
18
|
+
startIdentityFlow,
|
|
19
|
+
} from '../device-login.ts';
|
|
20
|
+
import { jsonError, jsonOk, type ToolTextResult } from './result.ts';
|
|
21
|
+
|
|
22
|
+
interface ActiveFlow {
|
|
23
|
+
flow: IdentityFlow;
|
|
24
|
+
/** 마지막 worker poll 시각 (epoch ms) — interval 준수 게이트. start 시각으로 초기화. */
|
|
25
|
+
lastPollAt: number;
|
|
26
|
+
/** slow_down 반영된 현재 interval. */
|
|
27
|
+
intervalMs: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let activeFlow: ActiveFlow | null = null;
|
|
31
|
+
|
|
32
|
+
function pendingPayload(flow: IdentityFlow, intervalMs: number): Record<string, unknown> {
|
|
33
|
+
return {
|
|
34
|
+
status: 'pending',
|
|
35
|
+
verification_uri_complete: flow.verificationUriComplete,
|
|
36
|
+
user_code: flow.userCode,
|
|
37
|
+
device_key_fingerprint: flow.fingerprint,
|
|
38
|
+
expires_in_seconds: Math.max(0, Math.round((flow.expiresAt - Date.now()) / 1000)),
|
|
39
|
+
retry_after_seconds: Math.ceil(intervalMs / 1000),
|
|
40
|
+
instructions:
|
|
41
|
+
'Show the user this URL and code. They open it in a browser, sign in (or sign up), verify ' +
|
|
42
|
+
'the fingerprint shown on screen matches device_key_fingerprint above (phishing guard), ' +
|
|
43
|
+
'and approve with their master password — it never leaves the browser. ' +
|
|
44
|
+
'Then call athsra_login_status (respect retry_after_seconds).',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** athsra_login_start — 브라우저 승인 flow 시작. 진행 중이면 같은 flow 재반환 (멱등). */
|
|
49
|
+
export async function handleLoginStart(): Promise<ToolTextResult> {
|
|
50
|
+
if (activeFlow && Date.now() < activeFlow.flow.expiresAt) {
|
|
51
|
+
return jsonOk({
|
|
52
|
+
...pendingPayload(activeFlow.flow, activeFlow.intervalMs),
|
|
53
|
+
note: 'a login flow is already in progress — showing the SAME pending flow (no new code minted).',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
let flow: IdentityFlow;
|
|
57
|
+
try {
|
|
58
|
+
flow = await startIdentityFlow();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return jsonError(err instanceof Error ? err.message : String(err));
|
|
61
|
+
}
|
|
62
|
+
activeFlow = { flow, lastPollAt: Date.now(), intervalMs: flow.intervalMs };
|
|
63
|
+
return jsonOk(pendingPayload(flow, flow.intervalMs));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** athsra_login_status — poll 1회 (interval 준수). approved 는 keyring 저장 완료 후 반환. */
|
|
67
|
+
export async function handleLoginStatus(): Promise<ToolTextResult> {
|
|
68
|
+
if (!activeFlow) {
|
|
69
|
+
return jsonError('no login flow in progress — call athsra_login_start first.');
|
|
70
|
+
}
|
|
71
|
+
const { flow } = activeFlow;
|
|
72
|
+
if (Date.now() >= flow.expiresAt) {
|
|
73
|
+
activeFlow = null;
|
|
74
|
+
return jsonOk({
|
|
75
|
+
status: 'expired',
|
|
76
|
+
hint: 'the approval window passed — call athsra_login_start to begin a new flow.',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// interval 준수 — 과속이면 worker 미타격, 캐시된 pending + retry_after 반환 (RFC 8628).
|
|
80
|
+
const sinceLastPoll = Date.now() - activeFlow.lastPollAt;
|
|
81
|
+
if (sinceLastPoll < activeFlow.intervalMs) {
|
|
82
|
+
return jsonOk({
|
|
83
|
+
...pendingPayload(flow, activeFlow.intervalMs),
|
|
84
|
+
retry_after_seconds: Math.ceil((activeFlow.intervalMs - sinceLastPoll) / 1000),
|
|
85
|
+
note: 'polled too fast — cached status returned without hitting the server.',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
activeFlow.lastPollAt = Date.now();
|
|
89
|
+
const step = await pollDeviceTokenOnce(flow.client, flow.deviceCode);
|
|
90
|
+
switch (step.step) {
|
|
91
|
+
case 'pending':
|
|
92
|
+
case 'network':
|
|
93
|
+
return jsonOk(pendingPayload(flow, activeFlow.intervalMs));
|
|
94
|
+
case 'slow_down':
|
|
95
|
+
activeFlow.intervalMs += 5000;
|
|
96
|
+
return jsonOk({
|
|
97
|
+
...pendingPayload(flow, activeFlow.intervalMs),
|
|
98
|
+
note: 'server asked to slow down — interval increased.',
|
|
99
|
+
});
|
|
100
|
+
case 'denied':
|
|
101
|
+
activeFlow = null;
|
|
102
|
+
return jsonOk({
|
|
103
|
+
status: 'denied',
|
|
104
|
+
hint: 'the user rejected the approval. Call athsra_login_start to retry if intended.',
|
|
105
|
+
});
|
|
106
|
+
case 'expired':
|
|
107
|
+
activeFlow = null;
|
|
108
|
+
return jsonOk({
|
|
109
|
+
status: 'expired',
|
|
110
|
+
hint: 'the approval window passed — call athsra_login_start to begin a new flow.',
|
|
111
|
+
});
|
|
112
|
+
case 'token': {
|
|
113
|
+
const result = step.result;
|
|
114
|
+
if (result.tokenType !== 'user') {
|
|
115
|
+
activeFlow = null;
|
|
116
|
+
return jsonError('unexpected service token for identity login — start over.');
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const { userId } = await completeIdentityLogin({
|
|
120
|
+
result,
|
|
121
|
+
devicePrivateKey: flow.devicePrivateKey,
|
|
122
|
+
machineId: flow.machineId,
|
|
123
|
+
workerUrl: flow.workerUrl,
|
|
124
|
+
existingCreatedAt: flow.existingCreatedAt,
|
|
125
|
+
});
|
|
126
|
+
activeFlow = null;
|
|
127
|
+
return jsonOk({
|
|
128
|
+
status: 'approved',
|
|
129
|
+
user_id: userId,
|
|
130
|
+
machine_id: flow.machineId,
|
|
131
|
+
note:
|
|
132
|
+
'identity key + token stored in the OS keyring — all athsra tools now work on this ' +
|
|
133
|
+
'machine. The master password was never on this device.',
|
|
134
|
+
});
|
|
135
|
+
} catch (err) {
|
|
136
|
+
activeFlow = null;
|
|
137
|
+
return jsonError(
|
|
138
|
+
`identity key unseal failed: ${err instanceof Error ? err.message : String(err)} — call athsra_login_start to retry.`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** test helpers — activeFlow 조작 (interval 게이트/상태 전이 검증용). */
|
|
146
|
+
export const __test = {
|
|
147
|
+
reset(): void {
|
|
148
|
+
activeFlow = null;
|
|
149
|
+
},
|
|
150
|
+
get(): ActiveFlow | null {
|
|
151
|
+
return activeFlow;
|
|
152
|
+
},
|
|
153
|
+
rewindLastPoll(): void {
|
|
154
|
+
if (activeFlow) activeFlow.lastPollAt = 0;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-tools/mask.ts — secret 값 마스킹 + 출력 scrub (value tier 안전 노출).
|
|
3
|
+
*
|
|
4
|
+
* value tier(ATHSRA_MCP_READ_VALUES) 도구가 평문을 다룰 때, 기본은 마스킹된
|
|
5
|
+
* descriptor 만 노출(존재/형태/무결성 확인 가능, 역산 불가). full 평문은 명시
|
|
6
|
+
* 게이트(opt-in + confirm) 통과 시에만. athsra_run 의 캡처 출력은 secret 값을
|
|
7
|
+
* 제거(scrub)해 자식이 echo 한 secret 이 응답으로 새지 않게 한다.
|
|
8
|
+
*/
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
|
|
11
|
+
export interface MaskedSecret {
|
|
12
|
+
masked: true;
|
|
13
|
+
key: string;
|
|
14
|
+
/** 앞 4자 (값 길이 8+ 일 때만 — 짧은 값 전체 추측 방지). 짧으면 ''. */
|
|
15
|
+
prefix: string;
|
|
16
|
+
length: number;
|
|
17
|
+
/** 전체 평문의 sha256 hex (값 비교·무결성 확인용, 역산 불가). */
|
|
18
|
+
sha256: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 값을 노출하지 않는 descriptor — 존재/형태/무결성 확인용. */
|
|
22
|
+
export function maskValue(key: string, value: string): MaskedSecret {
|
|
23
|
+
const sha256 = createHash('sha256').update(value, 'utf8').digest('hex');
|
|
24
|
+
const prefix = value.length >= 8 ? value.slice(0, 4) : '';
|
|
25
|
+
return { masked: true, key, prefix, length: value.length, sha256 };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 텍스트에서 알려진 secret 값을 ***REDACTED*** 로 치환한다. 8자 이상 값만(짧은
|
|
30
|
+
* 값은 오탐 위험). 긴 값부터 치환해 부분 매칭(짧은 값이 긴 값의 부분)을 피한다.
|
|
31
|
+
*/
|
|
32
|
+
export function scrubSecrets(text: string, plain: Record<string, string>): string {
|
|
33
|
+
const values = Object.values(plain)
|
|
34
|
+
.filter((v) => v.length >= 8)
|
|
35
|
+
.sort((a, b) => b.length - a.length);
|
|
36
|
+
let out = text;
|
|
37
|
+
for (const value of values) {
|
|
38
|
+
out = out.split(value).join('***REDACTED***');
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|