@skrr-ai/cli 0.1.48 → 0.1.50

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.
Files changed (117) hide show
  1. package/README.md +5 -4
  2. package/dist/base-command.js +16 -4
  3. package/dist/commands/agent-worker.d.ts +12 -0
  4. package/dist/commands/agent-worker.js +140 -0
  5. package/dist/commands/agents/create.js +14 -7
  6. package/dist/commands/agents/update.js +11 -5
  7. package/dist/commands/code/index.d.ts +1 -0
  8. package/dist/commands/code/index.js +49 -5
  9. package/dist/commands/code/run.js +5 -1
  10. package/dist/commands/commitments/cycles.d.ts +19 -0
  11. package/dist/commands/commitments/cycles.js +46 -0
  12. package/dist/commands/commitments/effective-policy.js +26 -1
  13. package/dist/commands/commitments/explain.d.ts +17 -0
  14. package/dist/commands/commitments/explain.js +40 -0
  15. package/dist/commands/goals/create.d.ts +1 -0
  16. package/dist/commands/goals/create.js +22 -0
  17. package/dist/commands/logout.js +12 -1
  18. package/dist/commands/machines/dedicated/index.js +1 -1
  19. package/dist/commands/machines/dedicated/sign-in.js +4 -1
  20. package/dist/commands/machines/dedicated/terminal/kill.d.ts +21 -0
  21. package/dist/commands/machines/dedicated/terminal/kill.js +55 -0
  22. package/dist/commands/machines/dedicated/terminal/ls.d.ts +22 -0
  23. package/dist/commands/machines/dedicated/terminal/ls.js +70 -0
  24. package/dist/commands/machines/dedicated/terminal/rename.d.ts +23 -0
  25. package/dist/commands/machines/dedicated/terminal/rename.js +64 -0
  26. package/dist/commands/machines/dedicated/terminal.d.ts +9 -0
  27. package/dist/commands/machines/dedicated/terminal.js +27 -2
  28. package/dist/commands/machines/hosted/connect.js +2 -2
  29. package/dist/commands/machines/hosted/destroy.js +1 -1
  30. package/dist/commands/machines/hosted/exec.js +1 -1
  31. package/dist/commands/machines/hosted/list.js +1 -1
  32. package/dist/commands/machines/hosted/pause.js +1 -1
  33. package/dist/commands/machines/hosted/pull.js +1 -1
  34. package/dist/commands/machines/hosted/resume.js +1 -1
  35. package/dist/commands/machines/hosted/start.js +2 -2
  36. package/dist/commands/machines/hosted/status.js +2 -1
  37. package/dist/commands/tasks/create.js +1 -0
  38. package/dist/commands/tasks/list.js +1 -0
  39. package/dist/commands/tasks/update.js +1 -0
  40. package/dist/lib/agent-config.d.ts +2 -0
  41. package/dist/lib/agent-config.js +3 -1
  42. package/dist/lib/api-fetch.js +19 -0
  43. package/dist/lib/auth-storage.d.ts +14 -0
  44. package/dist/lib/auth-storage.js +14 -0
  45. package/dist/lib/commitments.d.ts +17 -2
  46. package/dist/lib/commitments.js +114 -0
  47. package/dist/lib/daemon-installer.d.ts +6 -5
  48. package/dist/lib/daemon-installer.js +4 -17
  49. package/dist/lib/daemonBroker.d.ts +120 -31
  50. package/dist/lib/daemonBroker.js +313 -20
  51. package/dist/lib/dedicated-lease-command.d.ts +14 -0
  52. package/dist/lib/dedicated-lease-command.js +30 -1
  53. package/dist/lib/dedicated-machines.js +8 -25
  54. package/dist/lib/dedicated-service-command.d.ts +11 -3
  55. package/dist/lib/dedicated-service-command.js +20 -3
  56. package/dist/lib/dedicated-service.d.ts +46 -0
  57. package/dist/lib/dedicated-service.js +85 -7
  58. package/dist/lib/dedicated-terminal.d.ts +21 -0
  59. package/dist/lib/dedicated-terminal.js +104 -9
  60. package/dist/lib/first-party-harness-broker.d.ts +10 -0
  61. package/dist/lib/first-party-harness-broker.js +9 -0
  62. package/dist/lib/first-party-harness-doctor.js +41 -1
  63. package/dist/lib/first-party-harness-managed.d.ts +4 -3
  64. package/dist/lib/first-party-harness-project-trust.d.ts +125 -0
  65. package/dist/lib/first-party-harness-project-trust.js +364 -0
  66. package/dist/lib/first-party-harness.d.ts +9 -2
  67. package/dist/lib/first-party-harness.js +6 -5
  68. package/dist/lib/hosted-machines.d.ts +10 -1
  69. package/dist/lib/hosted-machines.js +36 -2
  70. package/dist/lib/login.js +16 -0
  71. package/dist/lib/machine-spend-cap.d.ts +23 -0
  72. package/dist/lib/machine-spend-cap.js +68 -0
  73. package/dist/lib/node-adapter.js +15 -3
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +90 -0
  75. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +113 -0
  76. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +47 -0
  77. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +69 -0
  78. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +109 -0
  79. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +171 -0
  80. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +1 -1
  81. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +1 -1
  82. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  83. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -2
  84. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +8 -3
  85. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +9 -4
  86. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +8 -9
  87. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.d.ts +6 -0
  88. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.js +32 -9
  89. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.d.ts +38 -0
  90. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.js +66 -1
  91. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.d.ts +81 -0
  92. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.js +87 -0
  93. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +6 -1
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +90 -0
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +105 -0
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +47 -0
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +66 -0
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +109 -0
  99. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +164 -0
  100. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +1 -1
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +1 -1
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +8 -3
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +9 -4
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +8 -9
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.d.ts +6 -0
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.js +31 -9
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.d.ts +38 -0
  110. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.js +64 -0
  111. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.d.ts +81 -0
  112. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.js +80 -0
  113. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +6 -1
  114. package/dist/node_modules/@skrr-ai/auth-core/package.json +41 -1
  115. package/dist/node_modules/@skrr-ai/data-provider/index.js +3413 -3343
  116. package/oclif.manifest.json +8930 -8466
  117. package/package.json +4 -1
package/dist/lib/login.js CHANGED
@@ -83,6 +83,22 @@ async function cliLogin(opts = {}) {
83
83
  baseURL: serverUrl,
84
84
  });
85
85
  if (brokerResult.ok) {
86
+ if (brokerResult.brokered) {
87
+ // Broker-mode hand-off: the daemon served a short-lived access token
88
+ // from a family IT owns. Nothing credential-shaped is persisted here
89
+ // — the token lives for this process and the next `skrr` re-redeems
90
+ // over loopback. An explicit PKCE/device-code login further down
91
+ // still writes a real user-owned family, unchanged.
92
+ console.log('');
93
+ console.log(" This machine is signed in through the platform's managed hand-off.");
94
+ return {
95
+ token: brokerResult.accessToken,
96
+ ...(brokerResult.accessExpiresAt !== undefined
97
+ ? { expiresAt: brokerResult.accessExpiresAt }
98
+ : {}),
99
+ flow: 'daemon-broker',
100
+ };
101
+ }
86
102
  console.log('');
87
103
  console.log(' Authorized via the skrr background service on this computer (no browser needed).');
88
104
  return {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The figures behind a machine monthly-cap refusal, for every machine product.
3
+ *
4
+ * `RUNTIME_SPENDING_LIMIT_EXCEEDED` carries the cap, this month's spend and
5
+ * where the refused action would take it, as integer cents in `details`. The
6
+ * dedicated CLI learned to print them in OSK-9552; the hosted CLI kept printing
7
+ * the bare sentence, "Projected Hosted Machine usage would exceed the monthly
8
+ * cap." A person who had just watched `skrr balance show` report a large
9
+ * balance with overage on then read that as "you have money but we won't take
10
+ * it" (OSK-11946). The machine cap is a SEPARATE ceiling from the plan
11
+ * allowance and the credits, and overage does not raise it; without figures and
12
+ * that sentence nothing on the screen says so.
13
+ */
14
+ export declare function formatCapCents(cents: number): string;
15
+ /**
16
+ * `Cap $500.00; spent this month $12.22; this would bring it to $512.22`, or ''
17
+ * when the body is not a cap refusal or carries no figures.
18
+ */
19
+ export declare function describeMonthlyCapFigures(body: string | undefined): string;
20
+ /** Whether `body` is a machine monthly-cap refusal at all. */
21
+ export declare function isMonthlyCapRefusal(body: string | undefined): boolean;
22
+ /** The one sentence that says which ceiling this is — and which it is not. */
23
+ export declare const MONTHLY_CAP_IS_SEPARATE = "This is the machine monthly cap, separate from your plan allowance and credits; overage does not raise it.";
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ /**
3
+ * The figures behind a machine monthly-cap refusal, for every machine product.
4
+ *
5
+ * `RUNTIME_SPENDING_LIMIT_EXCEEDED` carries the cap, this month's spend and
6
+ * where the refused action would take it, as integer cents in `details`. The
7
+ * dedicated CLI learned to print them in OSK-9552; the hosted CLI kept printing
8
+ * the bare sentence, "Projected Hosted Machine usage would exceed the monthly
9
+ * cap." A person who had just watched `skrr balance show` report a large
10
+ * balance with overage on then read that as "you have money but we won't take
11
+ * it" (OSK-11946). The machine cap is a SEPARATE ceiling from the plan
12
+ * allowance and the credits, and overage does not raise it; without figures and
13
+ * that sentence nothing on the screen says so.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.MONTHLY_CAP_IS_SEPARATE = void 0;
17
+ exports.formatCapCents = formatCapCents;
18
+ exports.describeMonthlyCapFigures = describeMonthlyCapFigures;
19
+ exports.isMonthlyCapRefusal = isMonthlyCapRefusal;
20
+ function formatCapCents(cents) {
21
+ const sign = cents < 0 ? '-' : '';
22
+ return `${sign}$${(Math.abs(cents) / 100).toFixed(2)}`;
23
+ }
24
+ /** The refusal's `code` and `details`, when the body is JSON; null otherwise. */
25
+ function parseRefusal(body) {
26
+ if (!body)
27
+ return null;
28
+ try {
29
+ const parsed = JSON.parse(body);
30
+ const details = parsed?.details && typeof parsed.details === 'object' && !Array.isArray(parsed.details)
31
+ ? parsed.details
32
+ : undefined;
33
+ return { code: typeof parsed?.code === 'string' ? parsed.code : undefined, details };
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ /**
40
+ * `Cap $500.00; spent this month $12.22; this would bring it to $512.22`, or ''
41
+ * when the body is not a cap refusal or carries no figures.
42
+ */
43
+ function describeMonthlyCapFigures(body) {
44
+ const refusal = parseRefusal(body);
45
+ if (refusal?.code !== 'RUNTIME_SPENDING_LIMIT_EXCEEDED' || !refusal.details)
46
+ return '';
47
+ const details = refusal.details;
48
+ const cents = (key) => {
49
+ const value = details[key];
50
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
51
+ };
52
+ const limit = cents('limitCents');
53
+ const spent = cents('currentSpendCents');
54
+ const projected = cents('projectedCents');
55
+ return [
56
+ limit !== null ? `Cap ${formatCapCents(limit)}` : '',
57
+ spent !== null ? `spent this month ${formatCapCents(spent)}` : '',
58
+ projected !== null ? `this would bring it to ${formatCapCents(projected)}` : '',
59
+ ]
60
+ .filter(Boolean)
61
+ .join('; ');
62
+ }
63
+ /** Whether `body` is a machine monthly-cap refusal at all. */
64
+ function isMonthlyCapRefusal(body) {
65
+ return parseRefusal(body)?.code === 'RUNTIME_SPENDING_LIMIT_EXCEEDED';
66
+ }
67
+ /** The one sentence that says which ceiling this is — and which it is not. */
68
+ exports.MONTHLY_CAP_IS_SEPARATE = 'This is the machine monthly cap, separate from your plan allowance and credits; overage does not raise it.';
@@ -271,10 +271,22 @@ function createNodeAdapter(opts = {}) {
271
271
  }
272
272
  }
273
273
  // Refresh the in-process override so the retry on the SAME request
274
- // sees the new token. writeToBackend (called inside the broker)
275
- // already wrote to the keychain/file — but the adapter caches the
276
- // credential and only re-reads when explicitly told to.
274
+ // sees the new token. For a durable mint, writeToBackend (called inside
275
+ // the broker) already wrote to the keychain/file — but the adapter
276
+ // caches the credential and only re-reads when explicitly told to.
277
277
  const accessToken = outcome.outcome.accessToken;
278
+ if (outcome.outcome.brokered) {
279
+ // Broker-mode hand-off: an in-memory access token, nothing persisted.
280
+ // 'env-token' keeps `ensureFreshCliCredential` from trying to rotate a
281
+ // stored family that does not exist; renewal re-redeems the descriptor.
282
+ setAdapterCredentialOverride({
283
+ token: accessToken,
284
+ source: 'env-token',
285
+ kind: (0, auth_core_1.classifyTokenKind)(accessToken),
286
+ });
287
+ onTokenRefresh?.(accessToken);
288
+ return true;
289
+ }
278
290
  setAdapterCredentialOverride({
279
291
  token: accessToken,
280
292
  source: outcome.outcome.storedIn === 'keychain' ? 'keychain' : 'file',
@@ -0,0 +1,90 @@
1
+ /**
2
+ * cliHandoffWire.ts — the ONE declaration of the CLI↔daemon hand-off wire
3
+ * contracts, shared by the daemon (writer/server) and the CLI (reader).
4
+ *
5
+ * Two contracts live here:
6
+ *
7
+ * 1. The hand-off descriptor file (`descriptor.json` on Dedicated guests,
8
+ * `daemon-island.<profile>.json` on laptops). Until now the shape was
9
+ * declared twice — `DedicatedCliHandoffDescriptor` daemon-side and
10
+ * `LocalBootstrap` CLI-side — two type declarations for one file format,
11
+ * the same drift disease `sessionPermissionAuthority` fixed.
12
+ * 2. The broker-mode redeem exchange: `POST /v1/auth/cli-handoff-token`
13
+ * request/response plus its error codes.
14
+ *
15
+ * Readers stay loose (unknown fields ignored, absent `handoffModes` reads as
16
+ * refresh-family-only) so a new field never breaks an older peer; writers
17
+ * must produce the full shape.
18
+ */
19
+ /**
20
+ * What a caller may redeem the descriptor secret for.
21
+ * - `refresh_family`: the legacy durable mint (90-day cli-scope family).
22
+ * - `access_token`: broker mode — a short-lived access token from a
23
+ * daemon-owned family; nothing durable leaves the daemon.
24
+ */
25
+ export declare const CLI_HANDOFF_MODES: readonly ["refresh_family", "access_token"];
26
+ export type CliHandoffMode = (typeof CLI_HANDOFF_MODES)[number];
27
+ /** Broker-mode redeem route on the daemon's loopback server. */
28
+ export declare const CLI_HANDOFF_TOKEN_PATH = "/v1/auth/cli-handoff-token";
29
+ /** Error codes this contract emits. String literals so both ends compare
30
+ * without importing a symbol they may not have. */
31
+ export declare const CLI_HANDOFF_TOKEN_ERROR_UNAVAILABLE = "HANDOFF_TOKEN_UNAVAILABLE";
32
+ /** Emitted by the LEGACY /v1/auth/cli-handoff route when a hand-off-secret
33
+ * caller asks for a refresh family the daemon no longer mints for it. */
34
+ export declare const CLI_HANDOFF_ERROR_BROKER_ONLY = "HANDOFF_BROKER_ONLY";
35
+ /**
36
+ * The descriptor file format. `version` is absent on the oldest island files
37
+ * and `1` on the first dedicated-guest descriptors; `handoffModes` first
38
+ * appears at version 2. Every field the CLI needs to dial is validated; the
39
+ * rest ride along untyped.
40
+ */
41
+ export interface CliHandoffDescriptor {
42
+ version?: number;
43
+ pid?: number;
44
+ daemonId?: string;
45
+ host?: string;
46
+ port?: number;
47
+ /** Base64 handshake secret — hand-off-only on guests, island on laptops. */
48
+ secret?: string;
49
+ /** Server URL the daemon is authenticated against. Older daemons omit it;
50
+ * readers refuse to broker without it (never mint against an unknown
51
+ * server). */
52
+ serverUrl?: string;
53
+ /** Daemon-authored epoch counter; bumped when the descriptor is republished. */
54
+ epoch?: number;
55
+ handoffModes?: CliHandoffMode[];
56
+ }
57
+ /**
58
+ * Parse an untrusted descriptor JSON value. Returns null when the fields the
59
+ * caller cannot proceed without — host, port, secret — are absent or
60
+ * mistyped. Unknown fields and future `version` values are tolerated: the
61
+ * reader ignores them, which is what makes additive evolution safe.
62
+ */
63
+ export declare function parseCliHandoffDescriptor(value: unknown): CliHandoffDescriptor | null;
64
+ /**
65
+ * Untrusted `handoffModes` input → known literals only, deduped. An
66
+ * unrecognized future mode is dropped rather than widening by coercion.
67
+ */
68
+ export declare function normalizeHandoffModes(value: unknown): CliHandoffMode[];
69
+ /**
70
+ * The versioned-default rule: a descriptor that never declares modes offers
71
+ * only the durable mint it has always offered. Mode negotiation is purely
72
+ * additive — a daemon advertises the new mode, it never un-advertises the
73
+ * old one by accident.
74
+ */
75
+ export declare function advertisedHandoffModes(d: CliHandoffDescriptor): CliHandoffMode[];
76
+ export declare function descriptorOffersAccessToken(d: CliHandoffDescriptor): boolean;
77
+ export interface CliHandoffTokenRequest {
78
+ /** Bypass the daemon broker's cached token (the 401-recovery path). */
79
+ forceRefresh?: boolean;
80
+ }
81
+ export interface CliHandoffTokenResponse {
82
+ accessToken: string;
83
+ /** Absolute expiry in ms since epoch, when the daemon knows it. */
84
+ accessExpiresAt?: number;
85
+ /** Server the minted credential belongs to — lets the caller verify it is
86
+ * about to use a token for the API it thinks it is talking to. */
87
+ serverUrl?: string;
88
+ }
89
+ /** Parse the redeem response; null when the token itself is absent. */
90
+ export declare function parseCliHandoffTokenResponse(value: unknown): CliHandoffTokenResponse | null;
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ /**
3
+ * cliHandoffWire.ts — the ONE declaration of the CLI↔daemon hand-off wire
4
+ * contracts, shared by the daemon (writer/server) and the CLI (reader).
5
+ *
6
+ * Two contracts live here:
7
+ *
8
+ * 1. The hand-off descriptor file (`descriptor.json` on Dedicated guests,
9
+ * `daemon-island.<profile>.json` on laptops). Until now the shape was
10
+ * declared twice — `DedicatedCliHandoffDescriptor` daemon-side and
11
+ * `LocalBootstrap` CLI-side — two type declarations for one file format,
12
+ * the same drift disease `sessionPermissionAuthority` fixed.
13
+ * 2. The broker-mode redeem exchange: `POST /v1/auth/cli-handoff-token`
14
+ * request/response plus its error codes.
15
+ *
16
+ * Readers stay loose (unknown fields ignored, absent `handoffModes` reads as
17
+ * refresh-family-only) so a new field never breaks an older peer; writers
18
+ * must produce the full shape.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.CLI_HANDOFF_ERROR_BROKER_ONLY = exports.CLI_HANDOFF_TOKEN_ERROR_UNAVAILABLE = exports.CLI_HANDOFF_TOKEN_PATH = exports.CLI_HANDOFF_MODES = void 0;
22
+ exports.parseCliHandoffDescriptor = parseCliHandoffDescriptor;
23
+ exports.normalizeHandoffModes = normalizeHandoffModes;
24
+ exports.advertisedHandoffModes = advertisedHandoffModes;
25
+ exports.descriptorOffersAccessToken = descriptorOffersAccessToken;
26
+ exports.parseCliHandoffTokenResponse = parseCliHandoffTokenResponse;
27
+ /**
28
+ * What a caller may redeem the descriptor secret for.
29
+ * - `refresh_family`: the legacy durable mint (90-day cli-scope family).
30
+ * - `access_token`: broker mode — a short-lived access token from a
31
+ * daemon-owned family; nothing durable leaves the daemon.
32
+ */
33
+ exports.CLI_HANDOFF_MODES = ['refresh_family', 'access_token'];
34
+ /** Broker-mode redeem route on the daemon's loopback server. */
35
+ exports.CLI_HANDOFF_TOKEN_PATH = '/v1/auth/cli-handoff-token';
36
+ /** Error codes this contract emits. String literals so both ends compare
37
+ * without importing a symbol they may not have. */
38
+ exports.CLI_HANDOFF_TOKEN_ERROR_UNAVAILABLE = 'HANDOFF_TOKEN_UNAVAILABLE';
39
+ /** Emitted by the LEGACY /v1/auth/cli-handoff route when a hand-off-secret
40
+ * caller asks for a refresh family the daemon no longer mints for it. */
41
+ exports.CLI_HANDOFF_ERROR_BROKER_ONLY = 'HANDOFF_BROKER_ONLY';
42
+ /**
43
+ * Parse an untrusted descriptor JSON value. Returns null when the fields the
44
+ * caller cannot proceed without — host, port, secret — are absent or
45
+ * mistyped. Unknown fields and future `version` values are tolerated: the
46
+ * reader ignores them, which is what makes additive evolution safe.
47
+ */
48
+ function parseCliHandoffDescriptor(value) {
49
+ if (value == null || typeof value !== 'object')
50
+ return null;
51
+ const v = value;
52
+ if (typeof v.host !== 'string' || v.host.length === 0)
53
+ return null;
54
+ if (typeof v.port !== 'number' || !Number.isInteger(v.port) || v.port <= 0)
55
+ return null;
56
+ if (typeof v.secret !== 'string' || v.secret.length === 0)
57
+ return null;
58
+ return {
59
+ ...(typeof v.version === 'number' ? { version: v.version } : {}),
60
+ ...(typeof v.pid === 'number' ? { pid: v.pid } : {}),
61
+ ...(typeof v.daemonId === 'string' ? { daemonId: v.daemonId } : {}),
62
+ host: v.host,
63
+ port: v.port,
64
+ secret: v.secret,
65
+ ...(typeof v.serverUrl === 'string' ? { serverUrl: v.serverUrl } : {}),
66
+ ...(typeof v.epoch === 'number' ? { epoch: v.epoch } : {}),
67
+ ...(Array.isArray(v.handoffModes)
68
+ ? { handoffModes: normalizeHandoffModes(v.handoffModes) }
69
+ : {}),
70
+ };
71
+ }
72
+ /**
73
+ * Untrusted `handoffModes` input → known literals only, deduped. An
74
+ * unrecognized future mode is dropped rather than widening by coercion.
75
+ */
76
+ function normalizeHandoffModes(value) {
77
+ if (!Array.isArray(value))
78
+ return [];
79
+ const out = [];
80
+ for (const m of value) {
81
+ if (exports.CLI_HANDOFF_MODES.includes(m) &&
82
+ !out.includes(m)) {
83
+ out.push(m);
84
+ }
85
+ }
86
+ return out;
87
+ }
88
+ /**
89
+ * The versioned-default rule: a descriptor that never declares modes offers
90
+ * only the durable mint it has always offered. Mode negotiation is purely
91
+ * additive — a daemon advertises the new mode, it never un-advertises the
92
+ * old one by accident.
93
+ */
94
+ function advertisedHandoffModes(d) {
95
+ const declared = normalizeHandoffModes(d.handoffModes);
96
+ return declared.length > 0 ? declared : ['refresh_family'];
97
+ }
98
+ function descriptorOffersAccessToken(d) {
99
+ return advertisedHandoffModes(d).includes('access_token');
100
+ }
101
+ /** Parse the redeem response; null when the token itself is absent. */
102
+ function parseCliHandoffTokenResponse(value) {
103
+ if (value == null || typeof value !== 'object')
104
+ return null;
105
+ const v = value;
106
+ if (typeof v.accessToken !== 'string' || v.accessToken.length === 0)
107
+ return null;
108
+ return {
109
+ accessToken: v.accessToken,
110
+ ...(typeof v.accessExpiresAt === 'number' ? { accessExpiresAt: v.accessExpiresAt } : {}),
111
+ ...(typeof v.serverUrl === 'string' ? { serverUrl: v.serverUrl } : {}),
112
+ };
113
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * credentialSession.ts — the shared credential-lifecycle kernel (policy half).
3
+ *
4
+ * A credential lifecycle is policy × mechanism. The policy — serve a cached
5
+ * token until its expiry-skew window, collapse concurrent acquires onto one
6
+ * in-flight operation, invalidate on a resource-side 401 — is identical for
7
+ * every surface and was being re-implemented per consumer (task token-broker,
8
+ * delegated-cli redemption, the human CLI refresh path). The mechanism —
9
+ * WHERE the durable secret lives and HOW a token is acquired — legitimately
10
+ * differs (in-memory daemon family vs file+envelope+lock vs loopback redeem)
11
+ * and stays behind the `acquire` seam.
12
+ *
13
+ * Deliberately thin: no persistence, no retry policy, no logging. Callers
14
+ * own their store and their error surfaces; this owns the cache/single-
15
+ * flight/invalidate invariants so they cannot drift between surfaces.
16
+ */
17
+ /** What an acquire returns. `expiresAtMs` absent means "no known expiry" —
18
+ * the token is served until `invalidate()` or an acquire replaces it. */
19
+ export interface AcquiredCredential {
20
+ accessToken: string;
21
+ /** Absolute expiry in ms since epoch, when the issuer tells us. */
22
+ expiresAtMs?: number;
23
+ }
24
+ export interface CredentialSessionOptions {
25
+ /**
26
+ * The mechanism. Called with `forceRefresh: true` when the caller asked to
27
+ * bypass the cache (the 401-recovery path) — implementations should renew
28
+ * rather than serve their own cache. A rejected promise propagates to every
29
+ * waiter and is never cached.
30
+ */
31
+ acquire: (forceRefresh: boolean) => Promise<AcquiredCredential>;
32
+ /** Serve the cached token until this many ms before expiry. Default 30s. */
33
+ skewMs?: number;
34
+ /** Injectable clock for tests. */
35
+ now?: () => number;
36
+ }
37
+ export interface CredentialSession {
38
+ /**
39
+ * Resolves a usable access token: the cached one while it is inside its
40
+ * freshness window, otherwise a single shared acquire. `forceRefresh`
41
+ * discards the cache first (use after a resource 401).
42
+ */
43
+ getToken(forceRefresh?: boolean): Promise<string>;
44
+ /** Drop the cached token; the next getToken() acquires. */
45
+ invalidate(): void;
46
+ }
47
+ export declare function createCredentialSession(opts: CredentialSessionOptions): CredentialSession;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /**
3
+ * credentialSession.ts — the shared credential-lifecycle kernel (policy half).
4
+ *
5
+ * A credential lifecycle is policy × mechanism. The policy — serve a cached
6
+ * token until its expiry-skew window, collapse concurrent acquires onto one
7
+ * in-flight operation, invalidate on a resource-side 401 — is identical for
8
+ * every surface and was being re-implemented per consumer (task token-broker,
9
+ * delegated-cli redemption, the human CLI refresh path). The mechanism —
10
+ * WHERE the durable secret lives and HOW a token is acquired — legitimately
11
+ * differs (in-memory daemon family vs file+envelope+lock vs loopback redeem)
12
+ * and stays behind the `acquire` seam.
13
+ *
14
+ * Deliberately thin: no persistence, no retry policy, no logging. Callers
15
+ * own their store and their error surfaces; this owns the cache/single-
16
+ * flight/invalidate invariants so they cannot drift between surfaces.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.createCredentialSession = createCredentialSession;
20
+ const DEFAULT_SKEW_MS = 30_000;
21
+ function createCredentialSession(opts) {
22
+ const skewMs = opts.skewMs ?? DEFAULT_SKEW_MS;
23
+ const now = opts.now ?? Date.now;
24
+ let cached = null;
25
+ let inFlight = null;
26
+ let inFlightForced = false;
27
+ // Monotonic generation so a superseded in-flight acquire (a forceRefresh
28
+ // launched while a plain acquire was still running) cannot write its stale
29
+ // result back over the newer one.
30
+ let generation = 0;
31
+ const fresh = (t) => t.expiresAtMs == null || now() < t.expiresAtMs - skewMs;
32
+ const acquireShared = (force) => {
33
+ // A forced acquire must not join a non-forced in-flight one — that would
34
+ // serve the very credential a 401 just invalidated. A forced in-flight
35
+ // DOES satisfy non-forced waiters: it is already renewing.
36
+ if (inFlight && (inFlightForced || !force))
37
+ return inFlight;
38
+ const gen = ++generation;
39
+ const p = opts.acquire(force).then((t) => {
40
+ if (gen === generation)
41
+ cached = t;
42
+ return t;
43
+ });
44
+ inFlight = p;
45
+ inFlightForced = force;
46
+ // `then(cleanup, cleanup)` not `.finally(cleanup)`: finally propagates the
47
+ // rejection into a derived promise nobody awaits — an unhandledRejection.
48
+ const cleanup = () => {
49
+ if (inFlight === p) {
50
+ inFlight = null;
51
+ inFlightForced = false;
52
+ }
53
+ };
54
+ p.then(cleanup, cleanup);
55
+ return p;
56
+ };
57
+ return {
58
+ async getToken(forceRefresh = false) {
59
+ if (forceRefresh)
60
+ cached = null;
61
+ if (cached && fresh(cached))
62
+ return cached.accessToken;
63
+ return (await acquireShared(forceRefresh)).accessToken;
64
+ },
65
+ invalidate() {
66
+ cached = null;
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Server-attested tool approval (OSK-11975 follow-up to the `manual` mode
3
+ * refusal on `daemon:tool:request`).
4
+ *
5
+ * A daemon category in `manual` mode means "a PERSON approves each call". On
6
+ * the harness-session path the daemon asks through the permission bridge; on
7
+ * the daemon-tool path a cloud agent's call used to be refused outright,
8
+ * because the request carried no approval and there was no channel back to a
9
+ * person. The attested-approval protocol closes that gap without weakening
10
+ * the guarantee:
11
+ *
12
+ * 1. The daemon announces `daemon_tool_attested_approval_v1` and its
13
+ * resolved per-category posture (`toolPermissionModes`) at register time.
14
+ * 2. Before proxying a call, the server reads the announced posture for the
15
+ * tool's category. `manual` means it must ask first — through the same
16
+ * permission surface (`canUseTool` → `permission:request`) an in-process
17
+ * SDK turn would have used — and only then dispatch, carrying an
18
+ * `attestedApproval` object on the request.
19
+ * 3. The daemon verifies the attestation's SHAPE and BINDING — it is bound
20
+ * to this exact (toolUseId, toolName, toolInput digest) and expires
21
+ * quickly — and allows the call. A missing or malformed attestation gets
22
+ * the same named refusal as before.
23
+ *
24
+ * What the daemon verifies is the binding, not the server's honesty: the
25
+ * request already arrives over the daemon's authenticated server channel, so
26
+ * the field IS the attestation. What the binding buys is that the model —
27
+ * which authors `toolInput` — cannot mint an approval, and an approval for
28
+ * one call cannot be replayed onto another.
29
+ */
30
+ /** Announced by a daemon that honours `attestedApproval` on daemon:tool:request. */
31
+ export declare const DAEMON_TOOL_ATTESTED_APPROVAL_CAPABILITY = "daemon_tool_attested_approval_v1";
32
+ /**
33
+ * Announced by a daemon that honours `toolPosture: 'read_only'` on
34
+ * daemon:tool:request — the daemon-tool-path expression of the wire `plan`
35
+ * session mode: reads run, every mutating category is refused with a named
36
+ * reason. Carries INTENT like `browserPolicy: 'read_only'` — the daemon owns
37
+ * what read-only means, so the server cannot widen the fence by shipping a
38
+ * wider value.
39
+ */
40
+ export declare const DAEMON_TOOL_READ_ONLY_CAPABILITY = "daemon_tool_read_only_v1";
41
+ /**
42
+ * Tool categories the daemon recognizes for permission purposes. This is the
43
+ * wire vocabulary the daemon's announced `toolPermissionModes` is keyed on —
44
+ * shared so the server's pre-dispatch check and the daemon's own enforcement
45
+ * classify a tool name identically (a third map was exactly the drift this
46
+ * package exists to prevent).
47
+ */
48
+ export type DaemonToolCategory = 'read' | 'write' | 'bash' | 'browser' | 'claude_session' | 'native_ui';
49
+ export declare const DAEMON_TOOL_CATEGORIES: readonly DaemonToolCategory[];
50
+ /** Map a tool name to its category. Unknown tools take the restrictive answer. */
51
+ export declare function categoryForDaemonTool(toolName: string): DaemonToolCategory;
52
+ /**
53
+ * The posture a daemon announces per category for the daemon-tool path:
54
+ * - `auto` — the call runs once guardrails pass; no attestation needed
55
+ * - `manual` — the call needs a person; the server must attach a fresh
56
+ * `attestedApproval` or the daemon refuses it
57
+ * - `delegated` — the category's own policy decides per action (browser,
58
+ * native UI); asking up front would ask the wrong question
59
+ */
60
+ export type DaemonToolPathPosture = 'auto' | 'manual' | 'delegated';
61
+ export type DaemonToolPermissionModes = Readonly<Partial<Record<DaemonToolCategory, DaemonToolPathPosture>>>;
62
+ /** The wire field values `toolPosture` accepts. Unknown values must be ignored. */
63
+ export type DaemonToolPosture = 'read_only';
64
+ /**
65
+ * How long an attestation stays valid. Short: it answers ONE person, about
66
+ * THIS call, moments ago — it is not a standing grant. The server asks and
67
+ * dispatches back-to-back, so the window only has to outlive publish latency.
68
+ */
69
+ export declare const ATTESTED_TOOL_APPROVAL_TTL_MS: number;
70
+ export interface AttestedToolApproval {
71
+ v: 1;
72
+ /** Binds the approval to this exact call — a different call gets nothing. */
73
+ toolUseId: string;
74
+ toolName: string;
75
+ /** SHA-256 of the canonical tool input (see `canonicalToolInputDigest`). */
76
+ inputSha256: string;
77
+ /** Who answered on the server's permission surface. */
78
+ decidedBy: 'user';
79
+ issuedAt: number;
80
+ expiresAt: number;
81
+ }
82
+ /** The digest both sides compute over `toolInput`. */
83
+ export declare function canonicalToolInputDigest(toolInput: unknown): string;
84
+ /** Server side: mint the attestation after a person approved THIS call. */
85
+ export declare function mintAttestedToolApproval(args: {
86
+ toolUseId: string;
87
+ toolName: string;
88
+ toolInput: unknown;
89
+ now?: number;
90
+ ttlMs?: number;
91
+ }): AttestedToolApproval;
92
+ export type AttestedToolApprovalVerdict = {
93
+ ok: true;
94
+ } | {
95
+ ok: false;
96
+ reason: 'absent' | 'malformed' | 'binding' | 'expired';
97
+ };
98
+ /**
99
+ * Daemon side: verify an attestation against the call it claims to approve.
100
+ * Untrusted wire input — every field is checked, and any mismatch is a plain
101
+ * "no approval", never a partial credit. `reason` is for daemon logs; the
102
+ * refusal text stays generic so a probing caller learns nothing new.
103
+ */
104
+ export declare function verifyAttestedToolApproval(approval: unknown, call: {
105
+ toolUseId: string;
106
+ toolName: string;
107
+ toolInput: unknown;
108
+ now?: number;
109
+ }): AttestedToolApprovalVerdict;