@abelxiaoxing/cadence 2.0.9 → 2.0.11

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.
@@ -218,15 +218,19 @@ function readBounded(path, max, packaged = false) {
218
218
  }
219
219
  }
220
220
  function gitEnvironment() {
221
+ const root = process.getuid?.() === 0;
221
222
  return {
222
- ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_"))),
223
+ ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_") && !(root && name === "SUDO_UID"))),
223
224
  GIT_OPTIONAL_LOCKS: "0",
224
225
  LC_ALL: "C"
225
226
  };
226
227
  }
228
+ function gitArguments(args) {
229
+ return process.platform !== "win32" && process.getuid?.() === 0 ? ["-c", "safe.directory=", "-c", "core.fsmonitor=false", ...args] : args;
230
+ }
227
231
  function resolveWorkspace(cwd) {
228
232
  const actual = realpathSync(cwd);
229
- const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
233
+ const result = spawnSync("git", gitArguments(["rev-parse", "--show-toplevel"]), {
230
234
  cwd: actual,
231
235
  env: gitEnvironment(),
232
236
  encoding: "utf8",
@@ -288,12 +292,17 @@ function contains(root, path) {
288
292
  const within = relative(root, resolve(path));
289
293
  return !within || !within.startsWith(`..${sep}`) && within !== ".." && !isAbsolute(within);
290
294
  }
295
+ function privateWorkspace(requested, stateHome = defaultStateHome()) {
296
+ return contains(join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1"), canonicalAmbientRoot(requested));
297
+ }
291
298
  class TaskStore {
292
299
  root;
293
300
  tasks;
294
301
  privateRoot;
295
302
  constructor(root, stateHome = defaultStateHome()) {
296
303
  this.root = realpathSync(root);
304
+ if (privateWorkspace(this.root, stateHome))
305
+ throw new Error("Private state cannot be a workspace");
297
306
  this.tasks = join(this.root, ".cadence", "assistant", "tasks");
298
307
  this.privateRoot = privateRootFor(this.root, stateHome);
299
308
  if (contains(this.root, this.privateRoot))
@@ -4019,16 +4028,24 @@ function decodeModelCandidate(value) {
4019
4028
  }
4020
4029
  function decodeModelCatalog(value) {
4021
4030
  canonicalJson({ kind: "ok", value }, LIMITS.pageBytes);
4022
- const o = closed(value, ["version", "selected", "status", "entries"]);
4031
+ const o = closed(value, [
4032
+ "version",
4033
+ "selected",
4034
+ "status",
4035
+ "entries",
4036
+ "withheld"
4037
+ ]);
4023
4038
  const entries = list(o.entries, LIMITS.maxPageEntries, decodeModelCandidate);
4024
4039
  const status = oneOf(o.status, ["complete", "capacity"]);
4025
- if (status === "capacity" && entries.length || new Set(entries.map((e) => canonicalJson([e.provider, e.model]))).size !== entries.length)
4040
+ const withheld = integer(o.withheld, 0, LIMITS.maxPageEntries);
4041
+ if (status === "capacity" && (entries.length || withheld) || entries.length + withheld > LIMITS.maxPageEntries || new Set(entries.map((e) => canonicalJson([e.provider, e.model]))).size !== entries.length)
4026
4042
  invalid();
4027
4043
  return {
4028
4044
  version: decodeChoiceVersion(o.version),
4029
4045
  selected: nullable(o.selected, decodeModelSelection),
4030
4046
  status,
4031
- entries
4047
+ entries,
4048
+ withheld
4032
4049
  };
4033
4050
  }
4034
4051
  // src/history/admission.ts
@@ -5225,6 +5242,22 @@ function emptyScan() {
5225
5242
  }
5226
5243
 
5227
5244
  // src/history/live.ts
5245
+ function scopedSnapshot(source, scope) {
5246
+ if (scope.kind === "task" || source.scope.kind === "child")
5247
+ return;
5248
+ if (!source.runs.some((run) => scope.kind === "run" ? run.runId === scope.id && run.childRunId === null : run.childRunId === scope.id))
5249
+ return;
5250
+ return decodeSnapshot({
5251
+ ...source,
5252
+ scope,
5253
+ runs: source.runs.filter((run) => scope.kind === "run" ? run.runId === scope.id : run.childRunId === scope.id),
5254
+ horizon: {
5255
+ ...source.horizon,
5256
+ historicalRuns: { ...source.horizon.historicalRuns, scope }
5257
+ }
5258
+ });
5259
+ }
5260
+
5228
5261
  class HistoryLive {
5229
5262
  writer;
5230
5263
  projection;
@@ -5338,8 +5371,10 @@ class HistoryLive {
5338
5371
  }
5339
5372
  }
5340
5373
  async drain() {
5341
- const scopes = [...this.pending.values()];
5374
+ const order = { task: 0, run: 1, child: 2 };
5375
+ const scopes = [...this.pending.values()].sort((a, b) => order[a.kind] - order[b.kind]);
5342
5376
  this.pending.clear();
5377
+ const sources = [];
5343
5378
  try {
5344
5379
  for (const scope of scopes) {
5345
5380
  if (this.closed)
@@ -5347,14 +5382,19 @@ class HistoryLive {
5347
5382
  if (scope.kind !== "run" && ![...this.viewers].some((v) => canonicalJson(v.scope) === canonicalJson(scope)))
5348
5383
  continue;
5349
5384
  let snapshot;
5385
+ for (let i = sources.length - 1;i >= 0 && !snapshot; i--)
5386
+ snapshot = scopedSnapshot(sources[i], scope);
5387
+ const reused = snapshot !== undefined;
5350
5388
  try {
5351
- snapshot = await this.projection.snapshot(scope);
5389
+ snapshot ??= await this.projection.snapshot(scope);
5352
5390
  } catch {
5353
5391
  for (const v of this.viewers)
5354
5392
  if (v.scope.id === scope.id)
5355
5393
  this.enqueue(v, this.watermark(v.scope));
5356
5394
  continue;
5357
5395
  }
5396
+ if (!reused && sources.length < LIMITS.viewers && (scope.kind === "task" || scopes.some((s) => s.kind === "child" && snapshot.runs.some((r) => r.childRunId === s.id))))
5397
+ sources.push(snapshot);
5358
5398
  const key = `${scope.kind}/${scope.id}`, old = this.rings.get(key);
5359
5399
  const update = (previousSeq, selected2 = scope, previousKeys) => decodeLiveEvent({
5360
5400
  kind: "update",
@@ -14800,7 +14840,13 @@ function localOrigin(policy, actualPort) {
14800
14840
  }
14801
14841
 
14802
14842
  // src/service/server-config.ts
14803
- import { lstatSync as lstatSync8, realpathSync as realpathSync3, statSync } from "node:fs";
14843
+ import {
14844
+ existsSync as existsSync2,
14845
+ lstatSync as lstatSync8,
14846
+ mkdirSync as mkdirSync3,
14847
+ realpathSync as realpathSync3,
14848
+ statSync
14849
+ } from "node:fs";
14804
14850
  import { dirname as dirname7, join as join10 } from "node:path";
14805
14851
 
14806
14852
  // skills/_shared/load-config.mjs
@@ -17959,19 +18005,22 @@ class OwnerModelContext {
17959
18005
  throw new ContractError("unsupported-context");
17960
18006
  count2 += runtime.getModels(provider).length;
17961
18007
  if (count2 > LIMITS.maxPageEntries)
17962
- return { status: "capacity", entries: [] };
18008
+ return { status: "capacity", entries: [], withheld: 0 };
17963
18009
  }
17964
18010
  const literals = this.catalogLiterals(current2);
17965
18011
  const entries = [];
18012
+ let withheld = 0;
17966
18013
  for (const provider of providers) {
17967
18014
  const models = runtime.getModels(provider);
17968
- if (entries.length + models.length > LIMITS.maxPageEntries)
18015
+ if (entries.length + withheld + models.length > LIMITS.maxPageEntries)
17969
18016
  throw new ContractError("capacity");
17970
18017
  for (const model of models) {
17971
18018
  if (model.provider !== provider)
17972
18019
  throw new ContractError("unsupported-context");
17973
- this.publicIdentifier(provider, literals);
17974
- this.publicIdentifier(model.id, literals);
18020
+ if (!this.isPublicIdentifier(provider, literals) || !this.isPublicIdentifier(model.id, literals)) {
18021
+ withheld++;
18022
+ continue;
18023
+ }
17975
18024
  const api = modelApis.find((api2) => api2 === model.api) ?? null;
17976
18025
  const levels = this.#thinking(model);
17977
18026
  if (levels.length > 7)
@@ -18018,7 +18067,7 @@ class OwnerModelContext {
18018
18067
  const after = this.catalogSource();
18019
18068
  if (after.auth.identity !== current2.auth.identity || after.auth.stamp !== current2.auth.stamp)
18020
18069
  throw new ContractError("stale");
18021
- return { status: "complete", entries };
18070
+ return { status: "complete", entries, withheld };
18022
18071
  } catch (error) {
18023
18072
  throw new ContractError(error instanceof ContractError ? error.code : "corrupt-selected");
18024
18073
  }
@@ -18089,8 +18138,11 @@ class OwnerModelContext {
18089
18138
  add(value);
18090
18139
  return [...literals];
18091
18140
  }
18141
+ isPublicIdentifier(value, literals) {
18142
+ return !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value) && diagnosticText(value, 512, literals) === value && sanitizeText(value, literals) === value;
18143
+ }
18092
18144
  publicIdentifier(value, literals) {
18093
- if (/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value) || diagnosticText(value, 512, literals) !== value || sanitizeText(value, literals) !== value)
18145
+ if (!this.isPublicIdentifier(value, literals))
18094
18146
  throw new ContractError("unsupported-context");
18095
18147
  }
18096
18148
  replaceSelection(input, beforeInstall) {
@@ -18917,7 +18969,7 @@ function captureChanges(root) {
18917
18969
  limits.push("Git observation time budget exhausted");
18918
18970
  return { stdout: Buffer.alloc(0) };
18919
18971
  }
18920
- const result = spawnSync2("git", ["-c", "core.quotepath=true", ...args], {
18972
+ const result = spawnSync2("git", gitArguments(["-c", "core.quotepath=true", ...args]), {
18921
18973
  cwd: root,
18922
18974
  encoding: "buffer",
18923
18975
  timeout: Math.max(1, deadline - Date.now()),
@@ -18965,7 +19017,7 @@ function captureChanges(root) {
18965
19017
  limits.push(`Unreadable/changing: ${name}`);
18966
19018
  }
18967
19019
  };
18968
- const probe = spawnSync2("git", ["rev-parse", "--is-inside-work-tree"], {
19020
+ const probe = spawnSync2("git", gitArguments(["rev-parse", "--is-inside-work-tree"]), {
18969
19021
  cwd: root,
18970
19022
  env: gitEnvironment(),
18971
19023
  timeout: 3000,
@@ -22871,7 +22923,7 @@ function refuseAt(stage, step) {
22871
22923
  }
22872
22924
  }
22873
22925
  async function createServiceHost(value, sourceBuild) {
22874
- const embedded = "ff017feffc6b6b4521e470d82514d0ca008e15472fa1442ec498b243b0f7cf87";
22926
+ const embedded = "c2bb81519387005a71e88a64364dff47d5c39dab17812833ca898bc51055e59d";
22875
22927
  if (embedded && sourceBuild)
22876
22928
  throw new ContractError("unsupported-context");
22877
22929
  const contractHash = decodeHash(embedded ?? sourceBuild?.contractHash);
package/src/task-shell.ts CHANGED
@@ -579,7 +579,7 @@ export function registerTaskShell(pi: ExtensionAPI): void {
579
579
  `${e.provider}/${e.model} • ${e.api ?? "unsupported"} • ${e.eligibility}`,
580
580
  );
581
581
  const selected = await ui.select(
582
- `Exact OWNER model (Pi conversation model is independent) • ${catalog.status}\nCurrent ${pretty(catalog.selected)}`,
582
+ `Exact OWNER model (Pi conversation model is independent) • ${catalog.status}\nCurrent ${pretty(catalog.selected)}${catalog.withheld ? `\n${catalog.withheld} withheld: a provider/model ID contains a credential value (such as an apiKey equal to the provider name) or control characters. Give each credential a value that matches no ID; restart the service after editing models.json.` : ""}`,
583
583
  [...labels, "Return"],
584
584
  );
585
585
  if (!selected || !labels.includes(selected)) return;
package/src/task-store.ts CHANGED
@@ -171,26 +171,44 @@ export function readBounded(
171
171
  }
172
172
  }
173
173
 
174
- /** Git observations must describe the physical cwd, not inherited alternate index/worktree overrides. */
174
+ /** Git observations describe the physical cwd, not inherited alternate
175
+ * index/worktree overrides. Root never borrows sudo's original user's trust. */
175
176
  export function gitEnvironment(): NodeJS.ProcessEnv {
177
+ const root = process.getuid?.() === 0;
176
178
  return {
177
179
  ...Object.fromEntries(
178
- Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_")),
180
+ Object.entries(process.env).filter(
181
+ ([name]) => !name.startsWith("GIT_") && !(root && name === "SUDO_UID"),
182
+ ),
179
183
  ),
180
184
  GIT_OPTIONAL_LOCKS: "0",
181
185
  LC_ALL: "C",
182
186
  };
183
187
  }
188
+ /** Observation is not permission to trust another account's repository.
189
+ * Even with fsmonitor/textconv/ext-diff disabled, status/diff can invoke clean
190
+ * or process filters. Root therefore requires Git's own ownership check, with
191
+ * no safe.directory exceptions (including inherited wildcards). No config is
192
+ * written and ordinary-user trust behavior stays unchanged. */
193
+ export function gitArguments(args: string[]): string[] {
194
+ return process.platform !== "win32" && process.getuid?.() === 0
195
+ ? ["-c", "safe.directory=", "-c", "core.fsmonitor=false", ...args]
196
+ : args;
197
+ }
184
198
  export function resolveWorkspace(cwd: string): { root: string; cwd: string } {
185
199
  const actual = realpathSync(cwd);
186
- const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
187
- cwd: actual,
188
- env: gitEnvironment(),
189
- encoding: "utf8",
190
- timeout: 3000,
191
- maxBuffer: 8192,
192
- windowsHide: true,
193
- });
200
+ const result = spawnSync(
201
+ "git",
202
+ gitArguments(["rev-parse", "--show-toplevel"]),
203
+ {
204
+ cwd: actual,
205
+ env: gitEnvironment(),
206
+ encoding: "utf8",
207
+ timeout: 3000,
208
+ maxBuffer: 8192,
209
+ windowsHide: true,
210
+ },
211
+ );
194
212
  // Remove only Git's record terminator, not valid whitespace in a path.
195
213
  const topLevel = result.stdout?.replace(
196
214
  process.platform === "win32" ? /\r?\n$/ : /\n$/,
@@ -274,6 +292,18 @@ export function stateHomeInside(
274
292
  const canonical = realpathSync(root);
275
293
  return contains(canonical, privateRootFor(canonical, stateHome));
276
294
  }
295
+ /** Also exclude projects *inside* our reserved private namespace. Root access
296
+ * does not turn private history/coordination into an ordinary engineering tree.
297
+ * Resolves missing tails without creating anything (for explicit project mkdir). */
298
+ export function privateWorkspace(
299
+ requested: string,
300
+ stateHome = defaultStateHome(),
301
+ ): boolean {
302
+ return contains(
303
+ join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1"),
304
+ canonicalAmbientRoot(requested),
305
+ );
306
+ }
277
307
  export const LANDING_WORKSPACE = "cadence";
278
308
  /** A directory that contains the private state home (the home directory and
279
309
  * its ancestors) can never be a workspace; use ~/cadence instead of refusing.
@@ -283,6 +313,8 @@ export function effectiveWorkspace(
283
313
  stateHome?: string,
284
314
  home = homedir(),
285
315
  ): { workspace: string; redirected: boolean } {
316
+ if (privateWorkspace(requested, stateHome))
317
+ throw new Error("Private state cannot be a workspace");
286
318
  if (!stateHomeInside(resolveWorkspace(requested).root, stateHome))
287
319
  return { workspace: requested, redirected: false };
288
320
  return { workspace: join(home, LANDING_WORKSPACE), redirected: true };
@@ -301,6 +333,8 @@ export class TaskStore {
301
333
  readonly privateRoot: string;
302
334
  constructor(root: string, stateHome = defaultStateHome()) {
303
335
  this.root = realpathSync(root);
336
+ if (privateWorkspace(this.root, stateHome))
337
+ throw new Error("Private state cannot be a workspace");
304
338
  this.tasks = join(this.root, ".cadence", "assistant", "tasks");
305
339
  this.privateRoot = privateRootFor(this.root, stateHome);
306
340
  if (contains(this.root, this.privateRoot))