@skrr-ai/cli 0.1.10 → 0.1.12

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 (120) hide show
  1. package/dist/base-command.d.ts +1 -13
  2. package/dist/base-command.js +78 -1
  3. package/dist/commands/browser/skill/show.js +7 -1
  4. package/dist/commands/code/index.d.ts +1 -0
  5. package/dist/commands/code/index.js +9 -1
  6. package/dist/commands/commitments/analytics/index.js +2 -0
  7. package/dist/commands/daemon/byok.d.ts +1 -0
  8. package/dist/commands/daemon/byok.js +2 -1
  9. package/dist/commands/daemon/install.d.ts +15 -0
  10. package/dist/commands/daemon/install.js +39 -2
  11. package/dist/commands/daemon/login.d.ts +31 -0
  12. package/dist/commands/daemon/login.js +56 -0
  13. package/dist/commands/daemon/restart.d.ts +9 -0
  14. package/dist/commands/daemon/restart.js +36 -0
  15. package/dist/commands/daemon/start.d.ts +1 -0
  16. package/dist/commands/daemon/start.js +2 -1
  17. package/dist/commands/daemon/status.d.ts +1 -0
  18. package/dist/commands/daemon/status.js +2 -1
  19. package/dist/commands/daemon/stop.d.ts +1 -0
  20. package/dist/commands/daemon/stop.js +2 -1
  21. package/dist/commands/daemon/uninstall.d.ts +1 -0
  22. package/dist/commands/daemon/uninstall.js +2 -1
  23. package/dist/commands/daemon/unlock.d.ts +9 -0
  24. package/dist/commands/daemon/unlock.js +33 -0
  25. package/dist/commands/goals/key-results/create.js +32 -1
  26. package/dist/commands/goals/key-results/update.d.ts +11 -0
  27. package/dist/commands/goals/key-results/update.js +80 -2
  28. package/dist/commands/goals/plan-now.d.ts +54 -2
  29. package/dist/commands/goals/plan-now.js +175 -18
  30. package/dist/commands/goals/planner-config.d.ts +60 -9
  31. package/dist/commands/goals/planner-config.js +82 -34
  32. package/dist/commands/goals/revisions.js +17 -0
  33. package/dist/commands/goals/show.d.ts +17 -0
  34. package/dist/commands/goals/show.js +90 -3
  35. package/dist/commands/login.js +53 -0
  36. package/dist/commands/spaces/create.js +2 -1
  37. package/dist/commands/spaces/index.js +9 -1
  38. package/dist/commands/spaces/list.d.ts +18 -0
  39. package/dist/commands/spaces/list.js +57 -7
  40. package/dist/commands/spaces/show.js +4 -1
  41. package/dist/commands/spaces/summary.d.ts +4 -0
  42. package/dist/commands/spaces/summary.js +77 -1
  43. package/dist/commands/spaces/update.d.ts +4 -0
  44. package/dist/commands/spaces/update.js +38 -1
  45. package/dist/commands/tasks/actionability.js +40 -1
  46. package/dist/commands/tasks/activity.d.ts +29 -0
  47. package/dist/commands/tasks/activity.js +47 -0
  48. package/dist/commands/tasks/complete.d.ts +47 -0
  49. package/dist/commands/tasks/complete.js +159 -12
  50. package/dist/commands/tasks/create.d.ts +26 -0
  51. package/dist/commands/tasks/create.js +60 -1
  52. package/dist/commands/tasks/events/append.d.ts +2 -0
  53. package/dist/commands/tasks/events/append.js +41 -10
  54. package/dist/commands/tasks/events/list.js +60 -9
  55. package/dist/commands/tasks/output.js +22 -2
  56. package/dist/commands/tasks/ready.d.ts +38 -0
  57. package/dist/commands/tasks/ready.js +37 -0
  58. package/dist/commands/tasks/runs.d.ts +22 -0
  59. package/dist/commands/tasks/runs.js +110 -2
  60. package/dist/commands/tasks/show.d.ts +28 -0
  61. package/dist/commands/tasks/show.js +61 -0
  62. package/dist/commands/tasks/timeline.d.ts +7 -0
  63. package/dist/commands/tasks/timeline.js +34 -3
  64. package/dist/commands/tasks/update.d.ts +20 -0
  65. package/dist/commands/tasks/update.js +38 -1
  66. package/dist/commands/whoami.d.ts +40 -0
  67. package/dist/commands/whoami.js +57 -9
  68. package/dist/commands/wiki/ls.d.ts +23 -0
  69. package/dist/commands/wiki/ls.js +63 -10
  70. package/dist/commands/wiki/mv.d.ts +70 -0
  71. package/dist/commands/wiki/mv.js +198 -4
  72. package/dist/commands/wiki/rm.js +12 -1
  73. package/dist/commands/wiki/write.js +35 -3
  74. package/dist/help.d.ts +27 -0
  75. package/dist/help.js +48 -0
  76. package/dist/hooks/command-not-found.d.ts +31 -0
  77. package/dist/hooks/command-not-found.js +12 -80
  78. package/dist/lib/command-miss.d.ts +60 -0
  79. package/dist/lib/command-miss.js +128 -0
  80. package/dist/lib/commitment-analytics.d.ts +10 -0
  81. package/dist/lib/commitment-analytics.js +10 -0
  82. package/dist/lib/config.js +6 -2
  83. package/dist/lib/daemon-installer.d.ts +62 -0
  84. package/dist/lib/daemon-installer.js +247 -0
  85. package/dist/lib/daemon-setup.d.ts +49 -0
  86. package/dist/lib/daemon-setup.js +103 -0
  87. package/dist/lib/daemonBroker.d.ts +7 -1
  88. package/dist/lib/daemonBroker.js +71 -0
  89. package/dist/lib/daemonHandoff.d.ts +47 -0
  90. package/dist/lib/daemonHandoff.js +304 -0
  91. package/dist/lib/exec-oversky.d.ts +30 -0
  92. package/dist/lib/exec-oversky.js +41 -0
  93. package/dist/lib/format.d.ts +7 -0
  94. package/dist/lib/format.js +27 -5
  95. package/dist/lib/login.js +22 -2
  96. package/dist/lib/sky-code.js +1 -1
  97. package/dist/lib/task-transcript.d.ts +9 -0
  98. package/dist/lib/task-transcript.js +13 -2
  99. package/dist/lib/tasks.d.ts +34 -0
  100. package/dist/lib/tasks.js +70 -1
  101. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.d.ts +50 -0
  102. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/configRoot.js +67 -0
  103. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -0
  104. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +21 -1
  105. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseKeys.d.ts +87 -0
  106. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseKeys.js +94 -0
  107. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.d.ts +161 -0
  108. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.js +235 -0
  109. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.d.ts +50 -0
  110. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/configRoot.js +59 -0
  111. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -0
  112. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -0
  113. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseKeys.d.ts +87 -0
  114. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseKeys.js +91 -0
  115. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.d.ts +161 -0
  116. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.js +227 -0
  117. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  118. package/dist/node_modules/@skrr-ai/data-provider/index.js +15 -4
  119. package/oclif.manifest.json +1831 -1598
  120. package/package.json +5 -3
@@ -34,6 +34,13 @@ export declare function truncate(s: string, max: number): string;
34
34
  * reads as missing instead of as the string `undefined`.
35
35
  */
36
36
  export declare function formatCell(value: unknown, placeholder?: string): string;
37
+ /**
38
+ * Collapse newlines/runs of whitespace so a value cannot break the table grid.
39
+ *
40
+ * Exported because `renderTable` applies it to EVERY cell, not just the ones
41
+ * that happened to come through `formatCell`.
42
+ */
43
+ export declare function collapseWhitespace(value: string): string;
37
44
  /**
38
45
  * Render an array of row objects as a column-aligned table.
39
46
  *
@@ -4,6 +4,7 @@ exports.displayWidth = displayWidth;
4
4
  exports.pad = pad;
5
5
  exports.truncate = truncate;
6
6
  exports.formatCell = formatCell;
7
+ exports.collapseWhitespace = collapseWhitespace;
7
8
  exports.renderTable = renderTable;
8
9
  exports.localStamp = localStamp;
9
10
  exports.localZone = localZone;
@@ -141,28 +142,49 @@ function stringifyCell(value, depth) {
141
142
  .filter(Boolean)
142
143
  .join(' ');
143
144
  }
144
- /** Collapse newlines/runs of whitespace so a value cannot break the table grid. */
145
+ /**
146
+ * Collapse newlines/runs of whitespace so a value cannot break the table grid.
147
+ *
148
+ * Exported because `renderTable` applies it to EVERY cell, not just the ones
149
+ * that happened to come through `formatCell`.
150
+ */
145
151
  function collapseWhitespace(value) {
146
152
  return value.replace(/\s+/g, ' ').trim();
147
153
  }
148
154
  function renderTable(rows, columns, log) {
149
155
  if (rows.length === 0)
150
156
  return;
157
+ // Every cell is collapsed ONCE, here, before anything measures it. A
158
+ // fixed-width cell cannot legally contain a newline, and the table is the
159
+ // only place that knows that — a caller passing `String(item.preview)` has
160
+ // no idea whether the API's preview is one line or twelve.
161
+ //
162
+ // It has to happen before the width pass, not just at render: `displayWidth`
163
+ // counts a newline as one column, so an embedded one made the measurement
164
+ // wrong as well as the output. `tasks complete` writes a multi-line
165
+ // `---TASK_OUTPUT---` comment, so `tasks timeline` exploded one row across
166
+ // three physical lines and lost column alignment for everything after it —
167
+ // on every task that had been completed, which is all of them.
168
+ //
169
+ // `verbatim` is exempt from CLIPPING, not from this: an unclipped newline
170
+ // breaks the grid exactly as badly, and an id column never contains one.
171
+ const cells = rows.map((row) => Object.fromEntries(columns.map((col) => [String(col.key), collapseWhitespace(String(row[col.key] ?? ''))])));
151
172
  const widths = {};
152
173
  for (const col of columns) {
153
174
  const key = String(col.key);
154
175
  const headerWidth = displayWidth(col.header);
155
- const dataWidth = Math.max(...rows.map((r) => displayWidth(String(r[col.key] ?? ''))));
176
+ const dataWidth = Math.max(0, ...cells.map((r) => displayWidth(r[key])));
156
177
  const natural = Math.max(headerWidth, dataWidth);
157
178
  widths[key] = col.maxWidth && !col.verbatim ? Math.min(natural, col.maxWidth) : natural;
158
179
  }
159
180
  const header = columns.map((col) => pad(col.header, widths[String(col.key)])).join(' ');
160
181
  log(header);
161
- for (const row of rows) {
182
+ for (const row of cells) {
162
183
  const line = columns
163
184
  .map((col) => {
164
- const raw = String(row[col.key] ?? '');
165
- const width = widths[String(col.key)];
185
+ const key = String(col.key);
186
+ const raw = row[key];
187
+ const width = widths[key];
166
188
  return pad(col.verbatim ? raw : truncate(raw, width), width);
167
189
  })
168
190
  .join(' ');
package/dist/lib/login.js CHANGED
@@ -52,6 +52,8 @@ async function cliLogin(opts = {}) {
52
52
  }
53
53
  const envForce = isTruthy(process.env.OVERSKY_FORCE_DEVICE_CODE);
54
54
  const ssh = (0, ssh_detect_1.detectSshSession)();
55
+ /** Set when the broker was refused for a reason that WAITING fixes. */
56
+ let brokerRateLimited = false;
55
57
  // --- Daemon-as-broker pre-flight ---------------------------------------
56
58
  // When the local `oversky` daemon is already authenticated for this user
57
59
  // on this machine, the cleanest UX is no second browser flow at all:
@@ -114,6 +116,20 @@ async function cliLogin(opts = {}) {
114
116
  // the PKCE flow opens a browser to a different URL than the
115
117
  // daemon. Continues to PKCE below — that's the right fallback for
116
118
  // "I'm logging into a different server than the daemon".
119
+ // Transient by construction — the ceiling is per 15-minute window — so the
120
+ // useful thing to say is "wait", not "here is a browser". An interactive
121
+ // user still gets the browser, because that genuinely works for them; the
122
+ // note exists so a surprise browser has a stated cause. Non-interactive
123
+ // callers get the real reason from the throw below instead of a sentence
124
+ // about terminals.
125
+ if (brokerResult.reason === 'rate_limited') {
126
+ brokerRateLimited = true;
127
+ if (!(0, ssh_detect_1.isNonInteractive)()) {
128
+ console.log('');
129
+ console.log(' The local skrr runtime has brokered several logins recently and the server is ' +
130
+ 'rate-limiting them. Falling back to the browser flow.');
131
+ }
132
+ }
117
133
  if (brokerResult.reason === 'base_url_mismatch' && brokerResult.daemonServerUrl) {
118
134
  console.log('');
119
135
  console.log(` The local skrr runtime is logged in to ${brokerResult.daemonServerUrl}, but skrr is configured for ${serverUrl}.`);
@@ -121,8 +137,12 @@ async function cliLogin(opts = {}) {
121
137
  }
122
138
  }
123
139
  if ((0, ssh_detect_1.isNonInteractive)()) {
124
- throw new Error('skrr login requires an interactive terminal or an SSH session. ' +
125
- 'For CI, use `skrr create-token` and set OVERSKY_TOKEN=osk_ci_...');
140
+ throw new Error(brokerRateLimited
141
+ ? 'The local skrr runtime is being rate-limited while brokering logins, and this ' +
142
+ 'shell cannot run the browser flow instead. The limit is per 15-minute window — ' +
143
+ 'retry shortly. For CI, use `skrr create-token` and set OVERSKY_TOKEN=osk_ci_...'
144
+ : 'skrr login requires an interactive terminal or an SSH session. ' +
145
+ 'For CI, use `skrr create-token` and set OVERSKY_TOKEN=osk_ci_...');
126
146
  }
127
147
  // --- Decision tree (first match wins) ---
128
148
  if (opts.forceDeviceCode) {
@@ -91,7 +91,7 @@ function managedEnginePath(exeName, env = process.env) {
91
91
  * hard-coded home would defeat the pinning {@link engineHome} relies on.
92
92
  */
93
93
  function legacyManagedEnginePath(exeName, env = process.env) {
94
- if (env.OVERSKY_CONFIG_DIR?.trim())
94
+ if ((0, auth_core_1.isConfigRootOverridden)(env))
95
95
  return null;
96
96
  return node_path_1.default.join((0, node_os_1.homedir)(), '.oversky', 'sky-code', 'bin', exeName);
97
97
  }
@@ -23,6 +23,15 @@ export type TaskOutputComment = {
23
23
  parsed: ParsedTaskOutput;
24
24
  /** Block lines the field parser could not attribute — see `unattributedBlockLines`. */
25
25
  unattributed: string[];
26
+ /**
27
+ * How many OLDER TASK_OUTPUT blocks this one supersedes.
28
+ *
29
+ * `tasks complete` can run twice on an already-done task, and each run posts
30
+ * its own block. Callers render the newest, so without this count a screen
31
+ * built from several completions is indistinguishable from one built from a
32
+ * single completion.
33
+ */
34
+ supersededCount: number;
26
35
  };
27
36
  /**
28
37
  * Mirror of `api/server/services/taskCompletionFormatting.js:parseTaskOutputBlock`.
@@ -130,21 +130,32 @@ async function fetchLatestTaskOutputComment(taskId, limit = 50) {
130
130
  const comments = response?.data ?? [];
131
131
  // listTaskComments returns newest-first; take the first parseable block so a
132
132
  // later "action items tracked" follow-up comment doesn't mask the real output.
133
+ // Keep walking after finding it, though — the REST are earlier completions,
134
+ // and their number is the only signal a caller has that the newest block is
135
+ // not the whole story.
136
+ let newest = null;
137
+ let blocks = 0;
133
138
  for (const comment of comments) {
134
139
  const body = String(comment.body ?? comment.content ?? '');
135
140
  const parsed = parseTaskOutputBlock(body);
136
141
  if (!parsed)
137
142
  continue;
138
- return {
143
+ blocks += 1;
144
+ if (newest)
145
+ continue;
146
+ newest = {
139
147
  commentId: String(comment.id ?? comment._id ?? '-'),
140
148
  createdAt: comment.createdAt ? String(comment.createdAt) : null,
141
149
  author: String(comment.authorAgentId ?? comment.authorUserId ?? comment.author ?? '-'),
142
150
  body,
143
151
  parsed,
144
152
  unattributed: unattributedBlockLines(body),
153
+ supersededCount: 0,
145
154
  };
146
155
  }
147
- return null;
156
+ if (newest)
157
+ newest.supersededCount = blocks - 1;
158
+ return newest;
148
159
  }
149
160
  /** Last message the agent (not the user) produced in the execution conversation. */
150
161
  function lastAssistantMessage(messages) {
@@ -194,3 +194,37 @@ export declare function normalizeBulkUpdateInput(input: unknown): Array<{
194
194
  id: string;
195
195
  patch: TaskRecord;
196
196
  }>;
197
+ /**
198
+ * Verdict strings the server recognises on a task completion.
199
+ *
200
+ * `--verdict` is free text: `skrr tasks complete <id> --verdict banana`
201
+ * succeeds and the value is permanently recorded, while `--status` beside it
202
+ * is enumerated and rejects a typo. So `--verdict aproved` closes a task with
203
+ * a verdict nothing matches, quietly.
204
+ *
205
+ * It cannot simply be enumerated, and that is the point worth recording:
206
+ *
207
+ * - A REVIEWER task legitimately passes a JSON object
208
+ * (`{"posture":"request_changes","confidence":0.82,"criteria":[…]}`) —
209
+ * documented in the task-execution prompt and consumed by
210
+ * `normalizeReviewPosture`. `options:` on the flag would refuse it.
211
+ * - That normaliser accepts synonyms, and when it recognises NOTHING it does
212
+ * not fail — it derives the posture from the criteria instead. So a typo
213
+ * silently becomes whatever the criteria imply.
214
+ *
215
+ * Hence a warning, not a refusal, over the set the server actually matches.
216
+ * Mirrored from `normalizeReviewPosture` in
217
+ * `api/server/services/TasksMCP/taskExecutionHelpers.js` and pinned against it
218
+ * by `task-verdict-parity.spec.ts`, the same way TASK_WORK_EVENT_KINDS is.
219
+ */
220
+ export declare const RECOGNIZED_TASK_VERDICTS: readonly ["request_changes", "changes_requested", "needs_changes", "needs_revision", "approved", "approve", "pass", "passed", "blocked"];
221
+ /** The server's own normalisation, so the same spellings match here. */
222
+ export declare function normalizeVerdict(value: string): string;
223
+ /**
224
+ * Does this verdict mean anything to the server?
225
+ *
226
+ * `UNVERIFIED` is platform-stamped rather than user-supplied, and a JSON
227
+ * verdict is the reviewer shape — both are legitimate and neither is in the
228
+ * synonym list.
229
+ */
230
+ export declare function verdictIsRecognized(value: string | undefined): boolean;
package/dist/lib/tasks.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TASK_WORK_EVENT_KINDS = exports.TASK_PRIORITIES = void 0;
3
+ exports.RECOGNIZED_TASK_VERDICTS = exports.TASK_WORK_EVENT_KINDS = exports.TASK_PRIORITIES = void 0;
4
4
  exports.releasePinRepairGuard = releasePinRepairGuard;
5
5
  exports.taskUpdateMutationGuard = taskUpdateMutationGuard;
6
6
  exports.executionIsolationFromWorktreeFlag = executionIsolationFromWorktreeFlag;
@@ -27,6 +27,8 @@ exports.formatTaskLabel = formatTaskLabel;
27
27
  exports.hasTaskAssignee = hasTaskAssignee;
28
28
  exports.normalizeTaskUpdateEntry = normalizeTaskUpdateEntry;
29
29
  exports.normalizeBulkUpdateInput = normalizeBulkUpdateInput;
30
+ exports.normalizeVerdict = normalizeVerdict;
31
+ exports.verdictIsRecognized = verdictIsRecognized;
30
32
  const data_provider_1 = require("@skrr-ai/data-provider");
31
33
  const prompt_1 = require("./prompt");
32
34
  const projects_1 = require("./projects");
@@ -715,3 +717,70 @@ function formatApiError(err) {
715
717
  return err.message;
716
718
  return String(err);
717
719
  }
720
+ /**
721
+ * Verdict strings the server recognises on a task completion.
722
+ *
723
+ * `--verdict` is free text: `skrr tasks complete <id> --verdict banana`
724
+ * succeeds and the value is permanently recorded, while `--status` beside it
725
+ * is enumerated and rejects a typo. So `--verdict aproved` closes a task with
726
+ * a verdict nothing matches, quietly.
727
+ *
728
+ * It cannot simply be enumerated, and that is the point worth recording:
729
+ *
730
+ * - A REVIEWER task legitimately passes a JSON object
731
+ * (`{"posture":"request_changes","confidence":0.82,"criteria":[…]}`) —
732
+ * documented in the task-execution prompt and consumed by
733
+ * `normalizeReviewPosture`. `options:` on the flag would refuse it.
734
+ * - That normaliser accepts synonyms, and when it recognises NOTHING it does
735
+ * not fail — it derives the posture from the criteria instead. So a typo
736
+ * silently becomes whatever the criteria imply.
737
+ *
738
+ * Hence a warning, not a refusal, over the set the server actually matches.
739
+ * Mirrored from `normalizeReviewPosture` in
740
+ * `api/server/services/TasksMCP/taskExecutionHelpers.js` and pinned against it
741
+ * by `task-verdict-parity.spec.ts`, the same way TASK_WORK_EVENT_KINDS is.
742
+ */
743
+ exports.RECOGNIZED_TASK_VERDICTS = [
744
+ 'request_changes',
745
+ 'changes_requested',
746
+ 'needs_changes',
747
+ 'needs_revision',
748
+ 'approved',
749
+ 'approve',
750
+ 'pass',
751
+ 'passed',
752
+ 'blocked',
753
+ ];
754
+ /** The server's own normalisation, so the same spellings match here. */
755
+ function normalizeVerdict(value) {
756
+ return String(value ?? '')
757
+ .trim()
758
+ .toLowerCase()
759
+ .replace(/[\s-]+/g, '_');
760
+ }
761
+ /**
762
+ * Does this verdict mean anything to the server?
763
+ *
764
+ * `UNVERIFIED` is platform-stamped rather than user-supplied, and a JSON
765
+ * verdict is the reviewer shape — both are legitimate and neither is in the
766
+ * synonym list.
767
+ */
768
+ function verdictIsRecognized(value) {
769
+ if (!value)
770
+ return true;
771
+ const raw = value.trim();
772
+ if (!raw)
773
+ return true;
774
+ if (raw.toUpperCase() === 'UNVERIFIED')
775
+ return true;
776
+ if (raw.startsWith('{') || raw.startsWith('[')) {
777
+ try {
778
+ JSON.parse(raw);
779
+ return true;
780
+ }
781
+ catch {
782
+ return false;
783
+ }
784
+ }
785
+ return exports.RECOGNIZED_TASK_VERDICTS.includes(normalizeVerdict(raw));
786
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The skrr root — the single definition of where this machine keeps its skrr
3
+ * state.
4
+ *
5
+ * It lives here because BOTH binaries need the same answer and neither owns it.
6
+ * `skrr` and `skrrd` keep separate credentials on purpose, but they share one
7
+ * root, and every place that re-derived it became a place they could disagree.
8
+ * They did, twice in one day:
9
+ *
10
+ * - The CLI's engine path honoured `OVERSKY_CONFIG_DIR` while its config did
11
+ * not, so a relocated root moved the engine and left the credentials in
12
+ * `$HOME`. Fixed by teaching `config.ts` the variable — which promptly
13
+ * re-created the split in the mirror image, because `sky-code.ts` still read
14
+ * only the old name.
15
+ * - The daemon then had the same shape, and fixing the CLI's half turned it
16
+ * into a CLI/daemon divergence: with the variable set, the two halves of one
17
+ * product disagreed about where the credentials live.
18
+ *
19
+ * Each of those was a correct local fix that created the next defect, because
20
+ * the root was being DERIVED rather than READ. Teaching every site the same list
21
+ * of variable names only defers the problem to whenever the list changes again —
22
+ * which is exactly what `SKRR_CONFIG_DIR` did.
23
+ *
24
+ * So: one function, and a boundary test that fails if anything re-derives it.
25
+ *
26
+ * `SKRR_CONFIG_DIR` is the spelling for anything new (root `CLAUDE.md`) and wins
27
+ * when both are set. `OVERSKY_CONFIG_DIR` keeps working because existing installs,
28
+ * the engine resolver and the daemon all read it, and breaking a documented
29
+ * override to tidy a name would be a worse trade than carrying two.
30
+ *
31
+ * Takes `env` rather than reading the global so a caller can resolve a root for a
32
+ * child process it is about to spawn, and so tests need no process-wide mutation.
33
+ */
34
+ export declare function resolveConfigRoot(env?: NodeJS.ProcessEnv): string;
35
+ /** A path under the skrr root, e.g. `configRootPath(env, 'sky-code', 'bin')`. */
36
+ export declare function configRootPath(env: NodeJS.ProcessEnv, ...segments: string[]): string;
37
+ /**
38
+ * Has the operator explicitly relocated the root?
39
+ *
40
+ * A distinct question from "what is the root", and it has a real caller: the
41
+ * legacy-location fallbacks exist to find state left by an older layout under
42
+ * `$HOME`, and once someone has POINTED the root somewhere, guessing at
43
+ * `$HOME` is no longer a helpful fallback — it is a different machine's data.
44
+ *
45
+ * Shared for the same reason as `resolveConfigRoot`: this check was copied to
46
+ * three call sites, each spelling the variable list itself, so adding
47
+ * `SKRR_CONFIG_DIR` to the resolver silently left the copies answering the old
48
+ * question.
49
+ */
50
+ export declare function isConfigRootOverridden(env?: NodeJS.ProcessEnv): boolean;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveConfigRoot = resolveConfigRoot;
7
+ exports.configRootPath = configRootPath;
8
+ exports.isConfigRootOverridden = isConfigRootOverridden;
9
+ const node_os_1 = require("node:os");
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ /**
12
+ * The skrr root — the single definition of where this machine keeps its skrr
13
+ * state.
14
+ *
15
+ * It lives here because BOTH binaries need the same answer and neither owns it.
16
+ * `skrr` and `skrrd` keep separate credentials on purpose, but they share one
17
+ * root, and every place that re-derived it became a place they could disagree.
18
+ * They did, twice in one day:
19
+ *
20
+ * - The CLI's engine path honoured `OVERSKY_CONFIG_DIR` while its config did
21
+ * not, so a relocated root moved the engine and left the credentials in
22
+ * `$HOME`. Fixed by teaching `config.ts` the variable — which promptly
23
+ * re-created the split in the mirror image, because `sky-code.ts` still read
24
+ * only the old name.
25
+ * - The daemon then had the same shape, and fixing the CLI's half turned it
26
+ * into a CLI/daemon divergence: with the variable set, the two halves of one
27
+ * product disagreed about where the credentials live.
28
+ *
29
+ * Each of those was a correct local fix that created the next defect, because
30
+ * the root was being DERIVED rather than READ. Teaching every site the same list
31
+ * of variable names only defers the problem to whenever the list changes again —
32
+ * which is exactly what `SKRR_CONFIG_DIR` did.
33
+ *
34
+ * So: one function, and a boundary test that fails if anything re-derives it.
35
+ *
36
+ * `SKRR_CONFIG_DIR` is the spelling for anything new (root `CLAUDE.md`) and wins
37
+ * when both are set. `OVERSKY_CONFIG_DIR` keeps working because existing installs,
38
+ * the engine resolver and the daemon all read it, and breaking a documented
39
+ * override to tidy a name would be a worse trade than carrying two.
40
+ *
41
+ * Takes `env` rather than reading the global so a caller can resolve a root for a
42
+ * child process it is about to spawn, and so tests need no process-wide mutation.
43
+ */
44
+ function resolveConfigRoot(env = process.env) {
45
+ const override = (env.SKRR_CONFIG_DIR || env.OVERSKY_CONFIG_DIR)?.trim();
46
+ return override || node_path_1.default.join((0, node_os_1.homedir)(), '.skrr');
47
+ }
48
+ /** A path under the skrr root, e.g. `configRootPath(env, 'sky-code', 'bin')`. */
49
+ function configRootPath(env, ...segments) {
50
+ return node_path_1.default.join(resolveConfigRoot(env), ...segments);
51
+ }
52
+ /**
53
+ * Has the operator explicitly relocated the root?
54
+ *
55
+ * A distinct question from "what is the root", and it has a real caller: the
56
+ * legacy-location fallbacks exist to find state left by an older layout under
57
+ * `$HOME`, and once someone has POINTED the root somewhere, guessing at
58
+ * `$HOME` is no longer a helpful fallback — it is a different machine's data.
59
+ *
60
+ * Shared for the same reason as `resolveConfigRoot`: this check was copied to
61
+ * three call sites, each spelling the variable list itself, so adding
62
+ * `SKRR_CONFIG_DIR` to the resolver silently left the copies answering the old
63
+ * question.
64
+ */
65
+ function isConfigRootOverridden(env = process.env) {
66
+ return Boolean((env.SKRR_CONFIG_DIR || env.OVERSKY_CONFIG_DIR)?.trim());
67
+ }
@@ -36,3 +36,6 @@ export { sanitizeSpawnEnv, SENSITIVE_ENV_VARS, SENSITIVE_ENV_PREFIXES, SENSITIVE
36
36
  export { CONFIG_DIR_NAME, LEGACY_CONFIG_DIR_NAMES, NATIVE_ID_PREFIX, LEGACY_NATIVE_ID_PREFIXES, DEFAULT_BINARY_NAME, } from './localIdentity.js';
37
37
  export { findLegacyLocalState, describeLegacyState, type LegacyStateFinding, type LegacyStateReport, } from './legacyStatePreflight.js';
38
38
  export { DEFAULT_SKY_CODE_CHANNEL, DEFAULT_SKY_CODE_FEED_BASE, SKY_CODE_CHANNELS, SKY_CODE_CHANNEL_ENV, SKY_CODE_MANIFEST_FILE, isSkyCodeChannel, resolveChannelManifestUrl, resolveSkyCodeChannel, skyCodeChannelPrefix, skyCodeFeedBase, SKY_CODE_FEED_BASE_ENV, type SkyCodeChannel, } from './skyCodeChannels.js';
39
+ export { OVERSKY_RELEASE_PUBLIC_KEYS, type ReleaseKey } from './releaseKeys.js';
40
+ export { MAX_SIGS, buildArtifactMessage, buildManifestMessage, getDeltaFor, verifyArtifact, verifyManifest, type DaemonVersionManifest, type DeltaDescriptor, type ManifestSignature, type PlatformArtifact, type VerifyResult, } from './releaseManifest.js';
41
+ export { resolveConfigRoot, configRootPath, isConfigRootOverridden } from './configRoot.js';
@@ -11,7 +11,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.challenge = exports.generateVerifier = exports.invalidateHeadersHelperCache = exports.runHeadersHelper = exports.SENTINEL_TRIGGERING_FAILURES = exports.__resetAuthHelperFailureReasonForTest = exports.getLastAuthHelperFailureReason = exports.getDefaultHelperTtlMs = exports.invokeAuthHelper = exports.AuthHelperUnavailableError = exports.AuthHelperUntrustedError = exports.CI_TOKEN_PREFIX = exports.classifyTokenKind = exports.invalidateAuthHelperCache = exports.__resetCredentialResolverForTest = exports.CREDENTIAL_PRECEDENCE = exports.CredentialResolver = exports.__setFdReaderForTest = exports.readAuthFromFd = exports.createRefreshScheduler = exports.UNKNOWN_401_BUDGET = exports.__resetUnknown401StreaksForTest = exports.__refreshDiagnostics = exports.installLockExitHandlers = exports.clearReauthState = exports.needsReauthState = exports.persistReauthState = exports.withAuthLock = exports.revokeDaemonRefreshSession = exports.refreshDaemonToken = exports.classifyRefreshResponse = exports.EXIT_NEEDS_REAUTH = exports.formatReauthMessage = exports.handleMidStreamAuthExpiry = exports.isAuthExpiredSseEvent = exports.decodeJwtPayload = exports.decodeJwtExpiry = exports.TransientAuthFailure = exports.PermanentAuthFailure = exports.emitAuthTelemetry = exports.getAuthBinaryName = exports.getAuthKillSwitches = exports.getAuthConfigDir = exports.getAuthLogger = exports.configureAuthCore = exports.OAUTH_CLI_SCOPE = exports.isDaemonScope = exports.VALID_DAEMON_SCOPE_SET = exports.ALL_DAEMON_SCOPES = exports.SCOPES = void 0;
12
12
  exports.tokenHash = exports.publicJwkFromX = exports.normalizeHtu = exports.jwkThumbprint = exports.generateDeviceKeyPair = exports.buildDeviceProof = exports.zeroizeKekCaches = exports.WindowsKek = exports.registerZeroizeHook = exports.MacosKek = exports.LinuxKek = exports.KekUnavailableError = exports.UnavailableKek = exports.InMemoryKek = exports.getKekStrategy = exports.__setKekStrategyForTest = exports.credEnvelopeWrap = exports.credEnvelopeUnwrap = exports.credEnvelopeSerialize = exports.credSealBuffer = exports.credOpenBuffer = exports.CRED_KEY_LEN = exports.isCredEnvelopeString = exports.generateDek = exports.CRED_ENVELOPE_MAGIC = exports.credEnvelopeEncrypt = exports.credEnvelopeDeserialize = exports.credEnvelopeDecrypt = exports.buildCredentialAad = exports.isValidRecoveryCode = exports.normalizeRecoveryCode = exports.parseRecoveryCode = exports.formatRecoveryCode = exports.MAX_TTL_DAYS = exports.MIN_TTL_DAYS = exports.parseTtlDays = exports.LoginExchangeError = exports.LoginInitError = exports.loginWithLocalhost = exports.LocalCallbackMalformedError = exports.LocalCallbackDeniedError = exports.LocalCallbackStateMismatchError = exports.LocalCallbackTimeoutError = exports.LocalCallbackBindError = exports.LocalCallbackError = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.startLocalCallback = exports.verifyChallenge = exports.safeEqual = exports.generateState = void 0;
13
13
  exports.getHarnessTier = exports.HARNESS_TIERS = exports.pairUint8ArrayToBase64Url = exports.pairUint8ArrayToBase64 = exports.parsePairUrl = exports.parsePairPlaintext = exports.parsePairBundle = exports.openPairBundleBase64 = exports.generatePairKeyPair = exports.buildPairUrl = exports.buildPairAad = exports.pairBase64UrlToUint8Array = exports.pairBase64ToUint8Array = exports.assemblePairBundleBase64 = exports.PairBundleError = exports.PAIR_SECRET_LEN = exports.PAIR_PUBKEY_LEN = exports.PAIR_NONCE_LEN = exports.PAIR_BUNDLE_MAX_BASE64_LEN = exports.PAIR_AAD_MAX_LEN = exports.PAIR_AAD_PREFIX = exports.__resetCachedDaemonIdForTest = exports.legacyDaemonUuids = exports.ensureMachineDaemonId = exports.__getDeviceIdentityStateForTest = exports.__setDeviceIdentityStateForTest = exports.__enrollmentBackoffPathForTest = exports.__enrollmentMarkerPathForTest = exports.__privateKeyPathForTest = exports.__publicKeyPathForTest = exports.describeDeviceIdentityState = exports.getDevicePublicKey = exports.isDeviceIdentityActive = exports.clearEnrollmentMarker = exports.enrollWithServer = exports.signServerRequest = exports.resetDeviceIdentity = exports.initDeviceIdentity = exports.__resetShutdownGuardForTest = exports.__getCredEnvelopeStateForTest = exports.__setCredEnvelopeStateForTest = exports.__wrappedDekFilePathForTest = exports.maybeDecryptOnRead = exports.maybeEncryptForWrite = exports.describeCredEnvelopeState = exports.isCredEnvelopeActive = exports.shutdownCredEnvelope = exports.resetCredEnvelope = exports.initCredEnvelope = exports.verifyDeviceProof = void 0;
14
- exports.SKY_CODE_FEED_BASE_ENV = exports.skyCodeFeedBase = exports.skyCodeChannelPrefix = exports.resolveSkyCodeChannel = exports.resolveChannelManifestUrl = exports.isSkyCodeChannel = exports.SKY_CODE_MANIFEST_FILE = exports.SKY_CODE_CHANNEL_ENV = exports.SKY_CODE_CHANNELS = exports.DEFAULT_SKY_CODE_FEED_BASE = exports.DEFAULT_SKY_CODE_CHANNEL = exports.describeLegacyState = exports.findLegacyLocalState = exports.DEFAULT_BINARY_NAME = exports.LEGACY_NATIVE_ID_PREFIXES = exports.NATIVE_ID_PREFIX = exports.LEGACY_CONFIG_DIR_NAMES = exports.CONFIG_DIR_NAME = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = exports.sanitizeSpawnEnv = exports.tierPermitsConfiguredAutoMode = exports.credentialPolicyForTier = void 0;
14
+ exports.isConfigRootOverridden = exports.configRootPath = exports.resolveConfigRoot = exports.verifyManifest = exports.verifyArtifact = exports.getDeltaFor = exports.buildManifestMessage = exports.buildArtifactMessage = exports.MAX_SIGS = exports.OVERSKY_RELEASE_PUBLIC_KEYS = exports.SKY_CODE_FEED_BASE_ENV = exports.skyCodeFeedBase = exports.skyCodeChannelPrefix = exports.resolveSkyCodeChannel = exports.resolveChannelManifestUrl = exports.isSkyCodeChannel = exports.SKY_CODE_MANIFEST_FILE = exports.SKY_CODE_CHANNEL_ENV = exports.SKY_CODE_CHANNELS = exports.DEFAULT_SKY_CODE_FEED_BASE = exports.DEFAULT_SKY_CODE_CHANNEL = exports.describeLegacyState = exports.findLegacyLocalState = exports.DEFAULT_BINARY_NAME = exports.LEGACY_NATIVE_ID_PREFIXES = exports.NATIVE_ID_PREFIX = exports.LEGACY_CONFIG_DIR_NAMES = exports.CONFIG_DIR_NAME = exports.SENSITIVE_ENV_PREFIX_EXCEPTIONS = exports.SENSITIVE_ENV_SUFFIXES = exports.SENSITIVE_ENV_PREFIXES = exports.SENSITIVE_ENV_VARS = exports.sanitizeSpawnEnv = exports.tierPermitsConfiguredAutoMode = exports.credentialPolicyForTier = void 0;
15
15
  // Daemon scope vocabulary — single source of truth for non-Mongoose tiers
16
16
  // (daemon, desktop, cli). Schema-bound canonical lives in
17
17
  // `@skrr-ai/data-schemas/common/daemonScopes`; the two are kept byte-equal
@@ -315,3 +315,23 @@ Object.defineProperty(exports, "resolveSkyCodeChannel", { enumerable: true, get:
315
315
  Object.defineProperty(exports, "skyCodeChannelPrefix", { enumerable: true, get: function () { return skyCodeChannels_js_1.skyCodeChannelPrefix; } });
316
316
  Object.defineProperty(exports, "skyCodeFeedBase", { enumerable: true, get: function () { return skyCodeChannels_js_1.skyCodeFeedBase; } });
317
317
  Object.defineProperty(exports, "SKY_CODE_FEED_BASE_ENV", { enumerable: true, get: function () { return skyCodeChannels_js_1.SKY_CODE_FEED_BASE_ENV; } });
318
+ // Daemon release trust: the pinned ed25519 keys and the canonical signature
319
+ // messages. Shared because BOTH the daemon (self-update) and the CLI
320
+ // (`skrr daemon install`, which runs when there is no daemon to ask) verify the
321
+ // same manifest against the same root. See releaseManifest.ts for why a mirror
322
+ // would be the wrong shape here.
323
+ var releaseKeys_js_1 = require("./releaseKeys.js");
324
+ Object.defineProperty(exports, "OVERSKY_RELEASE_PUBLIC_KEYS", { enumerable: true, get: function () { return releaseKeys_js_1.OVERSKY_RELEASE_PUBLIC_KEYS; } });
325
+ var releaseManifest_js_1 = require("./releaseManifest.js");
326
+ Object.defineProperty(exports, "MAX_SIGS", { enumerable: true, get: function () { return releaseManifest_js_1.MAX_SIGS; } });
327
+ Object.defineProperty(exports, "buildArtifactMessage", { enumerable: true, get: function () { return releaseManifest_js_1.buildArtifactMessage; } });
328
+ Object.defineProperty(exports, "buildManifestMessage", { enumerable: true, get: function () { return releaseManifest_js_1.buildManifestMessage; } });
329
+ Object.defineProperty(exports, "getDeltaFor", { enumerable: true, get: function () { return releaseManifest_js_1.getDeltaFor; } });
330
+ Object.defineProperty(exports, "verifyArtifact", { enumerable: true, get: function () { return releaseManifest_js_1.verifyArtifact; } });
331
+ Object.defineProperty(exports, "verifyManifest", { enumerable: true, get: function () { return releaseManifest_js_1.verifyManifest; } });
332
+ // The one definition of the skrr root, shared by `skrr` and `skrrd`. See the
333
+ // module for why re-deriving it kept producing divergences.
334
+ var configRoot_js_1 = require("./configRoot.js");
335
+ Object.defineProperty(exports, "resolveConfigRoot", { enumerable: true, get: function () { return configRoot_js_1.resolveConfigRoot; } });
336
+ Object.defineProperty(exports, "configRootPath", { enumerable: true, get: function () { return configRoot_js_1.configRootPath; } });
337
+ Object.defineProperty(exports, "isConfigRootOverridden", { enumerable: true, get: function () { return configRoot_js_1.isConfigRootOverridden; } });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * releaseKeys.ts — ed25519 public keys pinned into the daemon binary AND the
3
+ * CLI that installs it.
4
+ *
5
+ * These are the trust anchors the self-updater (`update.ts`) uses to verify
6
+ * the signed release manifest (`daemon/latest/daemon-version.json`) and the
7
+ * downloaded binary artifact OFFLINE, before swapping its own binary. The
8
+ * matching PRIVATE half lives ONLY in CI as the Secrets Manager secret
9
+ * `OVERSKY_RELEASE_SIGNING_KEY` (ed25519, base64 PKCS8) — it is never checked
10
+ * into the repo and never shipped in the binary.
11
+ *
12
+ * CURRENT STATE: a key is pinned (`daemon-2026-07`, provisioned 2026-07-13), so
13
+ * self-update signature verification is FAIL-CLOSED — an unsigned or
14
+ * badly-signed manifest or binary aborts the update.
15
+ *
16
+ * READ THE NEXT PARAGRAPH BEFORE EMPTYING THIS ARRAY.
17
+ *
18
+ * The empty array is not a safe default; it is a DISABLED SECURITY CONTROL.
19
+ * With no key pinned, `update.ts` logs a warning and proceeds on TLS + SHA256
20
+ * only — and the SHA256 comes from the same manifest fetched over the same
21
+ * channel, so it defends against corruption in transit, not against a
22
+ * compromised or spoofed update feed. That posture (mirrored from the desktop
23
+ * `MANIFEST_PUBLIC_KEY=''` default) existed because the keypair had not been
24
+ * minted yet. It has been minted. Removing the last entry below silently
25
+ * re-disables offline verification on every daemon that ships the change, and
26
+ * nothing in the build fails to tell you.
27
+ *
28
+ * To add a key (rotation, or re-provisioning after a compromise) run
29
+ * `node scripts/generate-release-keypair.mjs`. It mints the ed25519 keypair,
30
+ * prints the ready-to-paste `{ keyId, publicKeySpkiB64 }` line for the array
31
+ * below, and prints the commands to store the PRIVATE half in the GitHub
32
+ * secret `OVERSKY_RELEASE_SIGNING_KEY` + AWS Secrets Manager. Never commit the
33
+ * private half; the CI signer reads `OVERSKY_RELEASE_SIGNING_KEY` and MUST
34
+ * import `buildArtifactMessage` / `buildManifestMessage` from
35
+ * `release-manifest.ts` rather than reimplementing them, so signer and verifier
36
+ * can never drift. Add the *next* key alongside the current one BEFORE
37
+ * rotating, so an in-flight rotation verifies against either — the full
38
+ * procedure is the rotation runbook on the array below.
39
+ */
40
+ export interface ReleaseKey {
41
+ /** Human-readable key identifier, e.g. `daemon-2026-07`. Mirrors manifest `keyId`. */
42
+ keyId: string;
43
+ /** base64-encoded DER-SPKI ed25519 public key. */
44
+ publicKeySpkiB64: string;
45
+ }
46
+ /**
47
+ * Pinned release-signing public keys, newest first. Verification tries each
48
+ * key in turn (key rotation: keep current + next together during a rotation
49
+ * window). Non-empty → fail-closed, which is the state today. Empty →
50
+ * verification DISABLED with only a logged warning; see the file header before
51
+ * ever letting this array become empty.
52
+ *
53
+ * ZERO-DOWNTIME KEY-ROTATION RUNBOOK (config-only, no code change):
54
+ * 1. Mint the next keypair: `node scripts/generate-release-keypair.mjs --force`.
55
+ * 2. Pin its public half ABOVE the current key in the array below, and ship
56
+ * that daemon build so the fleet trusts BOTH keys.
57
+ * 3. Store the new private half as the GH Actions secret
58
+ * OVERSKY_RELEASE_SIGNING_KEY_NEXT (+ optional OVERSKY_RELEASE_KEY_ID_NEXT
59
+ * label) — the signer then DUAL-SIGNS every release, emitting the additive
60
+ * v2 `sigs[]` envelope (scalar `sig` stays = the current/primary key so
61
+ * daemons that haven't picked up step 2 yet still verify).
62
+ * 4. Cut one release during the overlap window; it verifies for daemons
63
+ * pinning the old key, the new key, or both (see release-manifest.test.ts
64
+ * "ROTATION headroom").
65
+ * 5. Once the new-key build has saturated the fleet: promote NEXT→primary
66
+ * (rename the secret to OVERSKY_RELEASE_SIGNING_KEY), drop the old key from
67
+ * the array, and remove OVERSKY_RELEASE_SIGNING_KEY_NEXT. No downtime, and
68
+ * no further code change — the multi-sig machinery is already shipped.
69
+ */
70
+ /**
71
+ * NOT ROTATED FOR THE skrr RENAME. Owner decision, 2026-08-31.
72
+ *
73
+ * A rename is not a compromise. Custody is already established (GH Actions
74
+ * secret `OVERSKY_RELEASE_SIGNING_KEY` + AWS Secrets Manager), the keyId
75
+ * `daemon-2026-07` is a date, not a brand, and no user ever sees any of it.
76
+ *
77
+ * The rotation procedure documented above is shipped and correct — it just has
78
+ * nothing to do here. Running it would cost a fleet-saturation window (step 5)
79
+ * and buy nothing, and the window is exactly when a self-update path is most
80
+ * fragile. Rotate on a compromise or an expiry, not on a rename.
81
+ *
82
+ * The signed message prefix (`oversky-daemon-manifest`) stays for the same
83
+ * reason: it lives inside the signed bytes, so changing it breaks verification
84
+ * between old and new peers with zero user-visible benefit — the same call made
85
+ * for `X-Oversky-Origin`.
86
+ */
87
+ export declare const OVERSKY_RELEASE_PUBLIC_KEYS: ReleaseKey[];