@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.
- package/README.md +92 -25
- package/docs/reuse-and-distribution.md +3 -3
- package/docs/service-management.md +84 -3
- package/package.json +1 -1
- package/src/native-build.ts +1 -1
- package/src/native-client.ts +15 -9
- package/src/operator-cli.mjs +13 -2
- package/src/sdk-worker.mjs +5 -1
- package/src/service/contracts/models.ts +14 -2
- package/src/service-cli.mjs +465 -75
- package/src/service-host.mjs +69 -17
- package/src/task-shell.ts +1 -1
- package/src/task-store.ts +44 -10
- package/web/dist/assets/cadence.css +1 -1
- package/web/dist/assets/cadence.js +7 -7
- package/web/dist/manifest.json +7 -7
package/src/service-host.mjs
CHANGED
|
@@ -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, [
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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.
|
|
17974
|
-
|
|
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 (
|
|
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 = "
|
|
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
|
|
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(
|
|
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(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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))
|