@abelxiaoxing/cadence 2.0.2 → 2.0.4
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 +52 -5
- package/docs/reuse-and-distribution.md +1 -1
- package/docs/service-management.md +18 -3
- package/package.json +5 -2
- package/src/agent-panel-cadence.ts +651 -0
- package/src/agent-panel-model.ts +521 -0
- package/src/agent-panel.ts +474 -0
- package/src/index.ts +12 -4
- package/src/native-build.ts +1 -1
- package/src/native-client.ts +125 -14
- package/src/service/contracts/models.ts +3 -5
- package/src/service/private-files.ts +18 -10
- package/src/service-cli.mjs +82 -27
- package/src/service-host.mjs +18 -15
- package/src/task-shell.ts +12 -9
- package/src/worker-public.mjs +3 -0
- package/web/dist/assets/cadence.css +1 -1
- package/web/dist/assets/cadence.js +7 -7
- package/web/dist/manifest.json +7 -7
|
@@ -26,10 +26,14 @@ export function refused(): never {
|
|
|
26
26
|
export function hash(bytes: string | Buffer): string {
|
|
27
27
|
return createHash("sha256").update(bytes).digest("hex");
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
/** Owner-only mode for state Cadence creates. External sources it only reads
|
|
30
|
+
* (Pi's agent directory and files) keep whatever mode the user's umask gave
|
|
31
|
+
* them, such as 0755/0775 from Pi itself; ownership is still required. */
|
|
32
|
+
function privateMode(st: Stats, ownerOnly = true): void {
|
|
30
33
|
if (
|
|
31
34
|
process.platform !== "win32" &&
|
|
32
|
-
((st.mode & 0o077) !== 0 ||
|
|
35
|
+
((ownerOnly && (st.mode & 0o077) !== 0) ||
|
|
36
|
+
(process.getuid && st.uid !== process.getuid()))
|
|
33
37
|
)
|
|
34
38
|
refused();
|
|
35
39
|
}
|
|
@@ -80,7 +84,7 @@ export class DirectoryWalks {
|
|
|
80
84
|
remember(path: string, st: Stats): void {
|
|
81
85
|
if (this.open()) this.walked.set(path, { dev: st.dev, ino: st.ino });
|
|
82
86
|
}
|
|
83
|
-
reuse(path: string): boolean {
|
|
87
|
+
reuse(path: string, ownerOnly = true): boolean {
|
|
84
88
|
const walked = this.open() ? this.walked.get(path) : undefined;
|
|
85
89
|
if (!walked) return false;
|
|
86
90
|
let st: Stats;
|
|
@@ -100,7 +104,7 @@ export class DirectoryWalks {
|
|
|
100
104
|
this.walked.delete(path);
|
|
101
105
|
return false;
|
|
102
106
|
}
|
|
103
|
-
privateMode(st);
|
|
107
|
+
privateMode(st, ownerOnly);
|
|
104
108
|
return true;
|
|
105
109
|
}
|
|
106
110
|
}
|
|
@@ -109,14 +113,18 @@ export class DirectoryWalks {
|
|
|
109
113
|
* or hostile-same-user guarantee; failed temporaries are deliberately preserved.
|
|
110
114
|
*/
|
|
111
115
|
export class PrivateFiles {
|
|
112
|
-
|
|
116
|
+
/** `ownerOnly: false` for an external namespace such as Pi's agent directory. */
|
|
117
|
+
constructor(
|
|
118
|
+
readonly namespace: string,
|
|
119
|
+
readonly ownerOnly = true,
|
|
120
|
+
) {}
|
|
113
121
|
directory(path: string, create = false, walks?: DirectoryWalks): boolean {
|
|
114
122
|
const tail = relative(this.namespace, path);
|
|
115
123
|
if (isAbsolute(tail) || tail === ".." || tail.startsWith(`..${sep}`))
|
|
116
124
|
refused();
|
|
117
125
|
if (!create) {
|
|
118
126
|
const full = resolve(path);
|
|
119
|
-
if (walks?.reuse(full)) return true;
|
|
127
|
+
if (walks?.reuse(full, this.ownerOnly)) return true;
|
|
120
128
|
const namespace = resolve(this.namespace);
|
|
121
129
|
let inside = false,
|
|
122
130
|
last: Stats | undefined;
|
|
@@ -126,7 +134,7 @@ export class PrivateFiles {
|
|
|
126
134
|
// including an unsafe parent before a missing child. Only the caller's
|
|
127
135
|
// DirectoryWalks keeps the result, for that same synchronous operation.
|
|
128
136
|
if (at === namespace) inside = true;
|
|
129
|
-
if (inside) privateMode(st);
|
|
137
|
+
if (inside) privateMode(st, this.ownerOnly);
|
|
130
138
|
last = st;
|
|
131
139
|
});
|
|
132
140
|
if (found && last) walks?.remember(full, last);
|
|
@@ -135,7 +143,7 @@ export class PrivateFiles {
|
|
|
135
143
|
// Walk the namespace and its ambient ancestors once, then check each
|
|
136
144
|
// private segment exactly once, creating a missing one privately.
|
|
137
145
|
if (!safeDirectory(this.namespace, true)) return false;
|
|
138
|
-
privateMode(lstatSync(this.namespace));
|
|
146
|
+
privateMode(lstatSync(this.namespace), this.ownerOnly);
|
|
139
147
|
let at = this.namespace;
|
|
140
148
|
for (const part of tail.split(sep).filter(Boolean)) {
|
|
141
149
|
at = join(at, part);
|
|
@@ -160,7 +168,7 @@ export class PrivateFiles {
|
|
|
160
168
|
}
|
|
161
169
|
if (!st.isDirectory() || st.isSymbolicLink())
|
|
162
170
|
throw new Error("Unsafe directory");
|
|
163
|
-
privateMode(st);
|
|
171
|
+
privateMode(st, this.ownerOnly);
|
|
164
172
|
}
|
|
165
173
|
return true;
|
|
166
174
|
}
|
|
@@ -179,7 +187,7 @@ export class PrivateFiles {
|
|
|
179
187
|
}
|
|
180
188
|
if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
|
|
181
189
|
refused();
|
|
182
|
-
privateMode(before);
|
|
190
|
+
privateMode(before, this.ownerOnly);
|
|
183
191
|
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
184
192
|
try {
|
|
185
193
|
const opened = fstatSync(fd);
|
package/src/service-cli.mjs
CHANGED
|
@@ -564,7 +564,8 @@ var thinkingLevels = [
|
|
|
564
564
|
"low",
|
|
565
565
|
"medium",
|
|
566
566
|
"high",
|
|
567
|
-
"xhigh"
|
|
567
|
+
"xhigh",
|
|
568
|
+
"max"
|
|
568
569
|
];
|
|
569
570
|
function decodeModelSelector(v) {
|
|
570
571
|
const o = closed(v, ["provider", "model", "api", "thinking"]);
|
|
@@ -2510,6 +2511,23 @@ function hostSystem(root) {
|
|
|
2510
2511
|
"--value"
|
|
2511
2512
|
]);
|
|
2512
2513
|
return r.status === 0 ? r.stdout.trim() === "yes" : undefined;
|
|
2514
|
+
},
|
|
2515
|
+
enableLinger: (interactive) => {
|
|
2516
|
+
const r = interactive ? spawnSync2("loginctl", ["enable-linger"], { stdio: "inherit" }) : spawnSync2("busctl", [
|
|
2517
|
+
"--system",
|
|
2518
|
+
"--allow-interactive-authorization=no",
|
|
2519
|
+
"--timeout=15",
|
|
2520
|
+
"call",
|
|
2521
|
+
"org.freedesktop.login1",
|
|
2522
|
+
"/org/freedesktop/login1",
|
|
2523
|
+
"org.freedesktop.login1.Manager",
|
|
2524
|
+
"SetUserLinger",
|
|
2525
|
+
"ubb",
|
|
2526
|
+
String(process.getuid?.()),
|
|
2527
|
+
"true",
|
|
2528
|
+
"false"
|
|
2529
|
+
], { stdio: "ignore", timeout: 20000 });
|
|
2530
|
+
return !r.error && r.status === 0;
|
|
2513
2531
|
}
|
|
2514
2532
|
};
|
|
2515
2533
|
}
|
|
@@ -2672,7 +2690,17 @@ function regularFile(path) {
|
|
|
2672
2690
|
return false;
|
|
2673
2691
|
}
|
|
2674
2692
|
}
|
|
2675
|
-
function
|
|
2693
|
+
function bootStart(s, attempt, interactive) {
|
|
2694
|
+
const current = s.linger();
|
|
2695
|
+
if (current === undefined)
|
|
2696
|
+
return {};
|
|
2697
|
+
if (current)
|
|
2698
|
+
return { linger: "on" };
|
|
2699
|
+
if (!attempt)
|
|
2700
|
+
return { linger: "off" };
|
|
2701
|
+
return { linger: s.enableLinger(interactive) ? "enabled" : "refused" };
|
|
2702
|
+
}
|
|
2703
|
+
function configureAutostart(s, mode, requested, linger = false) {
|
|
2676
2704
|
if (s.platform !== "linux")
|
|
2677
2705
|
return { status: "skipped" };
|
|
2678
2706
|
if (mode === "refresh" && (s.env.CADENCE_AUTOSTART === "0" || !piUserInstall(s)))
|
|
@@ -2714,11 +2742,12 @@ function configureAutostart(s, mode, requested) {
|
|
|
2714
2742
|
return { status: "failed", ...result };
|
|
2715
2743
|
if (!wanted && !active)
|
|
2716
2744
|
return { status: "disabled", ...result };
|
|
2745
|
+
const boot = () => wanted ? bootStart(s, mode === "install" ? linger : previous === undefined && s.env.CADENCE_LINGER !== "0", mode === "install") : {};
|
|
2717
2746
|
if (!regularFile(join2(agentDir, "auth.json")))
|
|
2718
|
-
return { status: "waiting-for-pi-login", ...result };
|
|
2747
|
+
return { status: "waiting-for-pi-login", ...result, ...boot() };
|
|
2719
2748
|
s.systemctl(["reset-failed", s.unit]);
|
|
2720
2749
|
const started = s.systemctl(["start", s.unit]).status === 0;
|
|
2721
|
-
return { status: started ? "started" : "failed", ...result };
|
|
2750
|
+
return { status: started ? "started" : "failed", ...result, ...boot() };
|
|
2722
2751
|
}
|
|
2723
2752
|
function removeAutostart(s) {
|
|
2724
2753
|
if (s.platform !== "linux")
|
|
@@ -2751,7 +2780,8 @@ function autostartStatus(s) {
|
|
|
2751
2780
|
status: s.systemctl(["is-active", s.unit]).stdout.trim() || "unknown",
|
|
2752
2781
|
enabled: s.systemctl(["is-enabled", s.unit]).stdout.trim() || "unknown",
|
|
2753
2782
|
unit: path,
|
|
2754
|
-
workspace: generatedWorkspace(previous)
|
|
2783
|
+
workspace: generatedWorkspace(previous),
|
|
2784
|
+
...bootStart(s, false, false)
|
|
2755
2785
|
};
|
|
2756
2786
|
}
|
|
2757
2787
|
|
|
@@ -2786,8 +2816,8 @@ function refused() {
|
|
|
2786
2816
|
function hash(bytes) {
|
|
2787
2817
|
return createHash2("sha256").update(bytes).digest("hex");
|
|
2788
2818
|
}
|
|
2789
|
-
function privateMode(st) {
|
|
2790
|
-
if (process.platform !== "win32" && ((st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
2819
|
+
function privateMode(st, ownerOnly = true) {
|
|
2820
|
+
if (process.platform !== "win32" && (ownerOnly && (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
2791
2821
|
refused();
|
|
2792
2822
|
}
|
|
2793
2823
|
function same(a, b) {
|
|
@@ -2822,7 +2852,7 @@ class DirectoryWalks {
|
|
|
2822
2852
|
if (this.open())
|
|
2823
2853
|
this.walked.set(path, { dev: st.dev, ino: st.ino });
|
|
2824
2854
|
}
|
|
2825
|
-
reuse(path) {
|
|
2855
|
+
reuse(path, ownerOnly = true) {
|
|
2826
2856
|
const walked = this.open() ? this.walked.get(path) : undefined;
|
|
2827
2857
|
if (!walked)
|
|
2828
2858
|
return false;
|
|
@@ -2839,15 +2869,17 @@ class DirectoryWalks {
|
|
|
2839
2869
|
this.walked.delete(path);
|
|
2840
2870
|
return false;
|
|
2841
2871
|
}
|
|
2842
|
-
privateMode(st);
|
|
2872
|
+
privateMode(st, ownerOnly);
|
|
2843
2873
|
return true;
|
|
2844
2874
|
}
|
|
2845
2875
|
}
|
|
2846
2876
|
|
|
2847
2877
|
class PrivateFiles {
|
|
2848
2878
|
namespace;
|
|
2849
|
-
|
|
2879
|
+
ownerOnly;
|
|
2880
|
+
constructor(namespace, ownerOnly = true) {
|
|
2850
2881
|
this.namespace = namespace;
|
|
2882
|
+
this.ownerOnly = ownerOnly;
|
|
2851
2883
|
}
|
|
2852
2884
|
directory(path, create = false, walks) {
|
|
2853
2885
|
const tail = relative2(this.namespace, path);
|
|
@@ -2855,7 +2887,7 @@ class PrivateFiles {
|
|
|
2855
2887
|
refused();
|
|
2856
2888
|
if (!create) {
|
|
2857
2889
|
const full = resolve3(path);
|
|
2858
|
-
if (walks?.reuse(full))
|
|
2890
|
+
if (walks?.reuse(full, this.ownerOnly))
|
|
2859
2891
|
return true;
|
|
2860
2892
|
const namespace = resolve3(this.namespace);
|
|
2861
2893
|
let inside = false, last;
|
|
@@ -2863,7 +2895,7 @@ class PrivateFiles {
|
|
|
2863
2895
|
if (at2 === namespace)
|
|
2864
2896
|
inside = true;
|
|
2865
2897
|
if (inside)
|
|
2866
|
-
privateMode(st);
|
|
2898
|
+
privateMode(st, this.ownerOnly);
|
|
2867
2899
|
last = st;
|
|
2868
2900
|
});
|
|
2869
2901
|
if (found && last)
|
|
@@ -2872,7 +2904,7 @@ class PrivateFiles {
|
|
|
2872
2904
|
}
|
|
2873
2905
|
if (!safeDirectory(this.namespace, true))
|
|
2874
2906
|
return false;
|
|
2875
|
-
privateMode(lstatSync3(this.namespace));
|
|
2907
|
+
privateMode(lstatSync3(this.namespace), this.ownerOnly);
|
|
2876
2908
|
let at = this.namespace;
|
|
2877
2909
|
for (const part of tail.split(sep3).filter(Boolean)) {
|
|
2878
2910
|
at = join3(at, part);
|
|
@@ -2892,7 +2924,7 @@ class PrivateFiles {
|
|
|
2892
2924
|
}
|
|
2893
2925
|
if (!st.isDirectory() || st.isSymbolicLink())
|
|
2894
2926
|
throw new Error("Unsafe directory");
|
|
2895
|
-
privateMode(st);
|
|
2927
|
+
privateMode(st, this.ownerOnly);
|
|
2896
2928
|
}
|
|
2897
2929
|
return true;
|
|
2898
2930
|
}
|
|
@@ -2909,7 +2941,7 @@ class PrivateFiles {
|
|
|
2909
2941
|
}
|
|
2910
2942
|
if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
|
|
2911
2943
|
refused();
|
|
2912
|
-
privateMode(before);
|
|
2944
|
+
privateMode(before, this.ownerOnly);
|
|
2913
2945
|
const fd = openSync2(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
2914
2946
|
try {
|
|
2915
2947
|
const opened = fstatSync2(fd);
|
|
@@ -5843,13 +5875,13 @@ function startupGuidance(result, input) {
|
|
|
5843
5875
|
return `Cadence refused its installed package files (web/dist or agents); reinstall the package rather than editing it.
|
|
5844
5876
|
`;
|
|
5845
5877
|
case "pi-agent-directory":
|
|
5846
|
-
return `Pi's agent directory ${agent} must be an existing real directory owned by you
|
|
5878
|
+
return `Pi's agent directory ${agent} must be an existing real directory (not a symlink) owned by you.
|
|
5847
5879
|
`;
|
|
5848
5880
|
case "pi-auth":
|
|
5849
|
-
return `${join8(agent, "auth.json")} must be your own single-link regular file
|
|
5881
|
+
return `${join8(agent, "auth.json")} must be your own single-link regular file holding valid Pi credentials; log in to Pi first.
|
|
5850
5882
|
`;
|
|
5851
5883
|
case "pi-models":
|
|
5852
|
-
return `${join8(agent, "models.json")} must be your own single-link regular file
|
|
5884
|
+
return `${join8(agent, "models.json")} must be your own single-link regular file using only fields Cadence's pinned Pi SDK supports; a field only newer Pi understands is refused.
|
|
5853
5885
|
`;
|
|
5854
5886
|
case "pi-runtime":
|
|
5855
5887
|
return `Loading Pi's SDK model runtime failed after the owner registered (optional SDK packages missing, or sources the SDK refuses); it stays quarantined.
|
|
@@ -5984,15 +6016,30 @@ function autostartGuidance(s, result) {
|
|
|
5984
6016
|
` : "";
|
|
5985
6017
|
return note + autostartStatusGuidance(s, result, name, cli, workspace, manage);
|
|
5986
6018
|
}
|
|
6019
|
+
function lingerGuidance(linger) {
|
|
6020
|
+
switch (linger) {
|
|
6021
|
+
case "enabled":
|
|
6022
|
+
return `Linger enabled: Cadence starts at boot and keeps running after logout (undo: loginctl disable-linger).
|
|
6023
|
+
`;
|
|
6024
|
+
case "off":
|
|
6025
|
+
return `Linger is off: Cadence starts at login and stops at logout. Start it at boot: loginctl enable-linger
|
|
6026
|
+
`;
|
|
6027
|
+
case "refused":
|
|
6028
|
+
return `Linger needs authorization: Cadence starts at login and stops at logout. Start it at boot: loginctl enable-linger (may ask for your password)
|
|
6029
|
+
`;
|
|
6030
|
+
default:
|
|
6031
|
+
return "";
|
|
6032
|
+
}
|
|
6033
|
+
}
|
|
5987
6034
|
function autostartStatusGuidance(s, result, name, cli, workspace, manage) {
|
|
5988
6035
|
switch (result.status) {
|
|
5989
6036
|
case "started": {
|
|
6037
|
+
const linger = lingerGuidance(result.linger);
|
|
5990
6038
|
if (result.service !== "alive")
|
|
5991
6039
|
return `${s.unit} started for ${result.workspace}, but the owner reports ${result.service}.
|
|
5992
6040
|
Inspect: node ${cli} diagnose --workspace ${workspace}
|
|
5993
6041
|
${manage}
|
|
5994
|
-
`;
|
|
5995
|
-
const linger = s.linger() === false ? "Linger is off: Cadence starts at login and stops at logout. `loginctl enable-linger` keeps it running and starts it at boot.\n" : "";
|
|
6042
|
+
${linger}`;
|
|
5996
6043
|
return `Cadence WebUI: ${result.endpoint} (${s.unit}, workspace ${result.workspace})
|
|
5997
6044
|
Enter with your private link: node ${cli} token-link --workspace ${workspace} --reveal
|
|
5998
6045
|
${manage}
|
|
@@ -6001,7 +6048,7 @@ ${linger}`;
|
|
|
6001
6048
|
case "waiting-for-pi-login":
|
|
6002
6049
|
return `${s.unit} is enabled for ${result.workspace}; it starts once Pi has ${join8(agentDirectory(s), "auth.json")}.
|
|
6003
6050
|
Log in to Pi, then: systemctl --user start ${name}
|
|
6004
|
-
`;
|
|
6051
|
+
${lingerGuidance(result.linger)}`;
|
|
6005
6052
|
case "disabled":
|
|
6006
6053
|
return `${s.unit} refreshed for ${result.workspace}; left disabled as configured.
|
|
6007
6054
|
`;
|
|
@@ -6019,7 +6066,7 @@ Log in to Pi, then: systemctl --user start ${name}
|
|
|
6019
6066
|
`;
|
|
6020
6067
|
case "failed":
|
|
6021
6068
|
return `Could not enable or start ${s.unit}; see systemctl --user status ${name} and journalctl --user -u ${name}.
|
|
6022
|
-
`;
|
|
6069
|
+
${lingerGuidance(result.linger)}`;
|
|
6023
6070
|
default:
|
|
6024
6071
|
return "";
|
|
6025
6072
|
}
|
|
@@ -6027,8 +6074,16 @@ Log in to Pi, then: systemctl --user start ${name}
|
|
|
6027
6074
|
async function autostartMain(argv, hostUrl) {
|
|
6028
6075
|
const action = argv.shift();
|
|
6029
6076
|
let requested;
|
|
6030
|
-
|
|
6031
|
-
|
|
6077
|
+
let linger = false;
|
|
6078
|
+
while (action === "install" && argv.length) {
|
|
6079
|
+
if (argv[0] === "--workspace" && argv[1] && requested === undefined)
|
|
6080
|
+
requested = resolve5(argv.splice(0, 2)[1]);
|
|
6081
|
+
else if (argv[0] === "--linger" && !linger) {
|
|
6082
|
+
argv.shift();
|
|
6083
|
+
linger = true;
|
|
6084
|
+
} else
|
|
6085
|
+
break;
|
|
6086
|
+
}
|
|
6032
6087
|
if (argv.length || !["install", "refresh", "remove", "status"].includes(action ?? ""))
|
|
6033
6088
|
throw new ContractError("invalid");
|
|
6034
6089
|
const s = hostSystem(dirname8(dirname8(fileURLToPath(hostUrl))));
|
|
@@ -6039,7 +6094,7 @@ async function autostartMain(argv, hostUrl) {
|
|
|
6039
6094
|
result = autostartStatus(s);
|
|
6040
6095
|
else {
|
|
6041
6096
|
try {
|
|
6042
|
-
result = configureAutostart(s, action === "install" ? "install" : "refresh", requested);
|
|
6097
|
+
result = configureAutostart(s, action === "install" ? "install" : "refresh", requested, linger);
|
|
6043
6098
|
} catch (error) {
|
|
6044
6099
|
if (error instanceof ContractError && error.code === "invalid")
|
|
6045
6100
|
process.stderr.write(`Cadence autostart needs an existing absolute workspace directory (--workspace or CADENCE_WORKSPACE).
|
|
@@ -6062,8 +6117,8 @@ async function managementMain(argv, hostUrl) {
|
|
|
6062
6117
|
const command = argv.shift();
|
|
6063
6118
|
if (command === "--help" && !argv.length) {
|
|
6064
6119
|
process.stdout.write(`cadence-service start|status|diagnose|stop|token-link --reveal|token-rotate|recover inspect|recover resolve [--workspace PATH] [--state-home PATH]
|
|
6065
|
-
cadence-service autostart install [--workspace PATH]|refresh|remove|status
|
|
6066
|
-
Start is explicit, or the Linux systemd user unit that Pi's user-scope install registers (CADENCE_AUTOSTART=0, disable or mask opts out). Default detached or --foreground. Start accepts --config, --agent-dir, --host, --port, --token-file.
|
|
6120
|
+
cadence-service autostart install [--workspace PATH] [--linger]|refresh|remove|status
|
|
6121
|
+
Start is explicit, or the Linux systemd user unit that Pi's user-scope install registers (CADENCE_AUTOSTART=0, disable or mask opts out); its first registration also enables linger for boot start when logind allows it without a password (CADENCE_LINGER=0 opts out), and --linger asks for it explicitly. Default detached or --foreground. Start accepts --config, --agent-dir, --host, --port, --token-file.
|
|
6067
6122
|
Return/quit detaches clients; only explicit Cancel/Stop requests stop. Unconfirmed/quarantined is not stopped.
|
|
6068
6123
|
Recovery resolve requires this local native terminal, exact unchanged dead-owner resources and separate process/credential/storage acknowledgments. No --yes, network recovery, killing, repair or cleanup.
|
|
6069
6124
|
`);
|
package/src/service-host.mjs
CHANGED
|
@@ -951,7 +951,8 @@ var thinkingLevels = [
|
|
|
951
951
|
"low",
|
|
952
952
|
"medium",
|
|
953
953
|
"high",
|
|
954
|
-
"xhigh"
|
|
954
|
+
"xhigh",
|
|
955
|
+
"max"
|
|
955
956
|
];
|
|
956
957
|
function decodeModelSelector(v) {
|
|
957
958
|
const o = closed(v, ["provider", "model", "api", "thinking"]);
|
|
@@ -2134,8 +2135,8 @@ function refused() {
|
|
|
2134
2135
|
function hash(bytes) {
|
|
2135
2136
|
return createHash3("sha256").update(bytes).digest("hex");
|
|
2136
2137
|
}
|
|
2137
|
-
function privateMode(st) {
|
|
2138
|
-
if (process.platform !== "win32" && ((st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
2138
|
+
function privateMode(st, ownerOnly = true) {
|
|
2139
|
+
if (process.platform !== "win32" && (ownerOnly && (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
2139
2140
|
refused();
|
|
2140
2141
|
}
|
|
2141
2142
|
function same(a, b) {
|
|
@@ -2170,7 +2171,7 @@ class DirectoryWalks {
|
|
|
2170
2171
|
if (this.open())
|
|
2171
2172
|
this.walked.set(path, { dev: st.dev, ino: st.ino });
|
|
2172
2173
|
}
|
|
2173
|
-
reuse(path) {
|
|
2174
|
+
reuse(path, ownerOnly = true) {
|
|
2174
2175
|
const walked = this.open() ? this.walked.get(path) : undefined;
|
|
2175
2176
|
if (!walked)
|
|
2176
2177
|
return false;
|
|
@@ -2187,15 +2188,17 @@ class DirectoryWalks {
|
|
|
2187
2188
|
this.walked.delete(path);
|
|
2188
2189
|
return false;
|
|
2189
2190
|
}
|
|
2190
|
-
privateMode(st);
|
|
2191
|
+
privateMode(st, ownerOnly);
|
|
2191
2192
|
return true;
|
|
2192
2193
|
}
|
|
2193
2194
|
}
|
|
2194
2195
|
|
|
2195
2196
|
class PrivateFiles {
|
|
2196
2197
|
namespace;
|
|
2197
|
-
|
|
2198
|
+
ownerOnly;
|
|
2199
|
+
constructor(namespace, ownerOnly = true) {
|
|
2198
2200
|
this.namespace = namespace;
|
|
2201
|
+
this.ownerOnly = ownerOnly;
|
|
2199
2202
|
}
|
|
2200
2203
|
directory(path, create = false, walks) {
|
|
2201
2204
|
const tail = relative2(this.namespace, path);
|
|
@@ -2203,7 +2206,7 @@ class PrivateFiles {
|
|
|
2203
2206
|
refused();
|
|
2204
2207
|
if (!create) {
|
|
2205
2208
|
const full = resolve2(path);
|
|
2206
|
-
if (walks?.reuse(full))
|
|
2209
|
+
if (walks?.reuse(full, this.ownerOnly))
|
|
2207
2210
|
return true;
|
|
2208
2211
|
const namespace = resolve2(this.namespace);
|
|
2209
2212
|
let inside = false, last;
|
|
@@ -2211,7 +2214,7 @@ class PrivateFiles {
|
|
|
2211
2214
|
if (at2 === namespace)
|
|
2212
2215
|
inside = true;
|
|
2213
2216
|
if (inside)
|
|
2214
|
-
privateMode(st);
|
|
2217
|
+
privateMode(st, this.ownerOnly);
|
|
2215
2218
|
last = st;
|
|
2216
2219
|
});
|
|
2217
2220
|
if (found && last)
|
|
@@ -2220,7 +2223,7 @@ class PrivateFiles {
|
|
|
2220
2223
|
}
|
|
2221
2224
|
if (!safeDirectory(this.namespace, true))
|
|
2222
2225
|
return false;
|
|
2223
|
-
privateMode(lstatSync2(this.namespace));
|
|
2226
|
+
privateMode(lstatSync2(this.namespace), this.ownerOnly);
|
|
2224
2227
|
let at = this.namespace;
|
|
2225
2228
|
for (const part of tail.split(sep2).filter(Boolean)) {
|
|
2226
2229
|
at = join2(at, part);
|
|
@@ -2240,7 +2243,7 @@ class PrivateFiles {
|
|
|
2240
2243
|
}
|
|
2241
2244
|
if (!st.isDirectory() || st.isSymbolicLink())
|
|
2242
2245
|
throw new Error("Unsafe directory");
|
|
2243
|
-
privateMode(st);
|
|
2246
|
+
privateMode(st, this.ownerOnly);
|
|
2244
2247
|
}
|
|
2245
2248
|
return true;
|
|
2246
2249
|
}
|
|
@@ -2257,7 +2260,7 @@ class PrivateFiles {
|
|
|
2257
2260
|
}
|
|
2258
2261
|
if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
|
|
2259
2262
|
refused();
|
|
2260
|
-
privateMode(before);
|
|
2263
|
+
privateMode(before, this.ownerOnly);
|
|
2261
2264
|
const fd = openSync2(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
2262
2265
|
try {
|
|
2263
2266
|
const opened = fstatSync2(fd);
|
|
@@ -4000,7 +4003,7 @@ function decodeModelCandidate(value) {
|
|
|
4000
4003
|
thinking: list(o.thinking, 7, (v) => {
|
|
4001
4004
|
const t = closed(v, ["level", "selectable"]);
|
|
4002
4005
|
return {
|
|
4003
|
-
level: oneOf(t.level,
|
|
4006
|
+
level: oneOf(t.level, thinkingLevels),
|
|
4004
4007
|
selectable: bool(t.selectable)
|
|
4005
4008
|
};
|
|
4006
4009
|
}),
|
|
@@ -4010,7 +4013,7 @@ function decodeModelCandidate(value) {
|
|
|
4010
4013
|
"auth-unavailable"
|
|
4011
4014
|
])
|
|
4012
4015
|
};
|
|
4013
|
-
if (new Set(candidate.thinking.map((t) => t.level)).size !== candidate.thinking.length || candidate.thinking.some((t) => t.selectable && (
|
|
4016
|
+
if (new Set(candidate.thinking.map((t) => t.level)).size !== candidate.thinking.length || candidate.thinking.some((t) => t.selectable && (candidate.api === null || candidate.eligibility !== "candidate")) || candidate.eligibility === "candidate" && !candidate.thinking.some((t) => t.selectable))
|
|
4014
4017
|
invalid();
|
|
4015
4018
|
return candidate;
|
|
4016
4019
|
}
|
|
@@ -17108,7 +17111,7 @@ function piSource(agentDir, home, cwd, env) {
|
|
|
17108
17111
|
const directory = join12(realpathSync4(dirname9(selected2)), selected2.slice(dirname9(selected2).length + 1));
|
|
17109
17112
|
if (!lstatSync8(directory).isDirectory() || lstatSync8(directory).isSymbolicLink())
|
|
17110
17113
|
refuse2();
|
|
17111
|
-
const files = new PrivateFiles(directory);
|
|
17114
|
+
const files = new PrivateFiles(directory, false);
|
|
17112
17115
|
if (!files.directory(directory))
|
|
17113
17116
|
refuse2();
|
|
17114
17117
|
return {
|
|
@@ -22606,7 +22609,7 @@ function refuseAt(stage, step) {
|
|
|
22606
22609
|
}
|
|
22607
22610
|
}
|
|
22608
22611
|
async function createServiceHost(value, sourceBuild) {
|
|
22609
|
-
const embedded = "
|
|
22612
|
+
const embedded = "ff017feffc6b6b4521e470d82514d0ca008e15472fa1442ec498b243b0f7cf87";
|
|
22610
22613
|
if (embedded && sourceBuild)
|
|
22611
22614
|
throw new ContractError("unsupported-context");
|
|
22612
22615
|
const contractHash = decodeHash(embedded ?? sourceBuild?.contractHash);
|
package/src/task-shell.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "@earendil-works/pi-tui";
|
|
12
12
|
import {
|
|
13
13
|
autostartHint,
|
|
14
|
+
departure,
|
|
14
15
|
MANAGEMENT_GUIDANCE,
|
|
15
16
|
NativeConnection,
|
|
16
17
|
} from "./native-client.ts";
|
|
@@ -98,10 +99,10 @@ export function registerTaskShell(pi: ExtensionAPI): void {
|
|
|
98
99
|
dismiss?.();
|
|
99
100
|
// Detach original attachment, not service jobs/provider/auth. Shutdown does
|
|
100
101
|
// not wait for their lifetime. Retained transport blocks uncertain reuse.
|
|
101
|
-
|
|
102
|
+
return connection.detach().catch(() => {});
|
|
102
103
|
};
|
|
103
104
|
pi.on("session_start", (_event, ctx) => {
|
|
104
|
-
depart();
|
|
105
|
+
void depart();
|
|
105
106
|
unsubscribe?.();
|
|
106
107
|
unsubscribe = undefined;
|
|
107
108
|
selectedTask = undefined;
|
|
@@ -112,15 +113,17 @@ export function registerTaskShell(pi: ExtensionAPI): void {
|
|
|
112
113
|
});
|
|
113
114
|
});
|
|
114
115
|
pi.on("session_shutdown", () => {
|
|
115
|
-
depart();
|
|
116
|
+
const leaving = depart();
|
|
116
117
|
unsubscribe?.();
|
|
117
118
|
unsubscribe = undefined;
|
|
119
|
+
return departure(leaving);
|
|
118
120
|
});
|
|
119
|
-
|
|
120
|
-
pi.on("
|
|
121
|
-
pi.on("
|
|
122
|
-
pi.on("
|
|
123
|
-
pi.on("
|
|
121
|
+
const leave = () => void depart();
|
|
122
|
+
pi.on("session_before_switch", leave);
|
|
123
|
+
pi.on("session_before_fork", leave);
|
|
124
|
+
pi.on("session_before_tree", leave);
|
|
125
|
+
pi.on("session_before_compact", leave);
|
|
126
|
+
pi.on("model_select", leave);
|
|
124
127
|
// Only local dialog input is owned. Detached service work never owns Pi's
|
|
125
128
|
// conversation/tool lifetime or silently cancels it.
|
|
126
129
|
pi.on("input", () => (modal ? { action: "handled" as const } : undefined));
|
|
@@ -587,7 +590,7 @@ export function registerTaskShell(pi: ExtensionAPI): void {
|
|
|
587
590
|
"Exact thinking level • no clamping/fallback",
|
|
588
591
|
model.thinking.filter((t) => t.selectable).map((t) => t.level),
|
|
589
592
|
);
|
|
590
|
-
if (!level
|
|
593
|
+
if (!level) return;
|
|
591
594
|
// Decode via the shared model-choice method; never inspect parent auth.
|
|
592
595
|
const choice = {
|
|
593
596
|
provider: model.provider,
|
package/src/worker-public.mjs
CHANGED
|
@@ -411,6 +411,8 @@ export const modelApis = [
|
|
|
411
411
|
"mistral-conversations",
|
|
412
412
|
"azure-openai-responses",
|
|
413
413
|
];
|
|
414
|
+
// The pinned SDK's ThinkingLevel, including "max" (models whose
|
|
415
|
+
// thinkingLevelMap maps only max, such as some DeepSeek configurations).
|
|
414
416
|
export const thinkingLevels = [
|
|
415
417
|
"off",
|
|
416
418
|
"minimal",
|
|
@@ -418,6 +420,7 @@ export const thinkingLevels = [
|
|
|
418
420
|
"medium",
|
|
419
421
|
"high",
|
|
420
422
|
"xhigh",
|
|
423
|
+
"max",
|
|
421
424
|
];
|
|
422
425
|
export function decodeModelSelector(v) {
|
|
423
426
|
const o = closed(v, ["provider", "model", "api", "thinking"]);
|