@abelxiaoxing/cadence 2.0.2 → 2.0.3
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 +45 -5
- package/docs/reuse-and-distribution.md +1 -1
- package/docs/service-management.md +15 -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-client.ts +125 -14
- package/src/service/private-files.ts +18 -10
- package/src/service-cli.mjs +80 -26
- package/src/service-host.mjs +13 -11
- package/src/task-shell.ts +11 -8
- package/web/dist/assets/cadence.css +1 -1
- package/web/dist/assets/cadence.js +7 -7
- package/web/dist/manifest.json +6 -6
|
@@ -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
|
@@ -2510,6 +2510,23 @@ function hostSystem(root) {
|
|
|
2510
2510
|
"--value"
|
|
2511
2511
|
]);
|
|
2512
2512
|
return r.status === 0 ? r.stdout.trim() === "yes" : undefined;
|
|
2513
|
+
},
|
|
2514
|
+
enableLinger: (interactive) => {
|
|
2515
|
+
const r = interactive ? spawnSync2("loginctl", ["enable-linger"], { stdio: "inherit" }) : spawnSync2("busctl", [
|
|
2516
|
+
"--system",
|
|
2517
|
+
"--allow-interactive-authorization=no",
|
|
2518
|
+
"--timeout=15",
|
|
2519
|
+
"call",
|
|
2520
|
+
"org.freedesktop.login1",
|
|
2521
|
+
"/org/freedesktop/login1",
|
|
2522
|
+
"org.freedesktop.login1.Manager",
|
|
2523
|
+
"SetUserLinger",
|
|
2524
|
+
"ubb",
|
|
2525
|
+
String(process.getuid?.()),
|
|
2526
|
+
"true",
|
|
2527
|
+
"false"
|
|
2528
|
+
], { stdio: "ignore", timeout: 20000 });
|
|
2529
|
+
return !r.error && r.status === 0;
|
|
2513
2530
|
}
|
|
2514
2531
|
};
|
|
2515
2532
|
}
|
|
@@ -2672,7 +2689,17 @@ function regularFile(path) {
|
|
|
2672
2689
|
return false;
|
|
2673
2690
|
}
|
|
2674
2691
|
}
|
|
2675
|
-
function
|
|
2692
|
+
function bootStart(s, attempt, interactive) {
|
|
2693
|
+
const current = s.linger();
|
|
2694
|
+
if (current === undefined)
|
|
2695
|
+
return {};
|
|
2696
|
+
if (current)
|
|
2697
|
+
return { linger: "on" };
|
|
2698
|
+
if (!attempt)
|
|
2699
|
+
return { linger: "off" };
|
|
2700
|
+
return { linger: s.enableLinger(interactive) ? "enabled" : "refused" };
|
|
2701
|
+
}
|
|
2702
|
+
function configureAutostart(s, mode, requested, linger = false) {
|
|
2676
2703
|
if (s.platform !== "linux")
|
|
2677
2704
|
return { status: "skipped" };
|
|
2678
2705
|
if (mode === "refresh" && (s.env.CADENCE_AUTOSTART === "0" || !piUserInstall(s)))
|
|
@@ -2714,11 +2741,12 @@ function configureAutostart(s, mode, requested) {
|
|
|
2714
2741
|
return { status: "failed", ...result };
|
|
2715
2742
|
if (!wanted && !active)
|
|
2716
2743
|
return { status: "disabled", ...result };
|
|
2744
|
+
const boot = () => wanted ? bootStart(s, mode === "install" ? linger : previous === undefined && s.env.CADENCE_LINGER !== "0", mode === "install") : {};
|
|
2717
2745
|
if (!regularFile(join2(agentDir, "auth.json")))
|
|
2718
|
-
return { status: "waiting-for-pi-login", ...result };
|
|
2746
|
+
return { status: "waiting-for-pi-login", ...result, ...boot() };
|
|
2719
2747
|
s.systemctl(["reset-failed", s.unit]);
|
|
2720
2748
|
const started = s.systemctl(["start", s.unit]).status === 0;
|
|
2721
|
-
return { status: started ? "started" : "failed", ...result };
|
|
2749
|
+
return { status: started ? "started" : "failed", ...result, ...boot() };
|
|
2722
2750
|
}
|
|
2723
2751
|
function removeAutostart(s) {
|
|
2724
2752
|
if (s.platform !== "linux")
|
|
@@ -2751,7 +2779,8 @@ function autostartStatus(s) {
|
|
|
2751
2779
|
status: s.systemctl(["is-active", s.unit]).stdout.trim() || "unknown",
|
|
2752
2780
|
enabled: s.systemctl(["is-enabled", s.unit]).stdout.trim() || "unknown",
|
|
2753
2781
|
unit: path,
|
|
2754
|
-
workspace: generatedWorkspace(previous)
|
|
2782
|
+
workspace: generatedWorkspace(previous),
|
|
2783
|
+
...bootStart(s, false, false)
|
|
2755
2784
|
};
|
|
2756
2785
|
}
|
|
2757
2786
|
|
|
@@ -2786,8 +2815,8 @@ function refused() {
|
|
|
2786
2815
|
function hash(bytes) {
|
|
2787
2816
|
return createHash2("sha256").update(bytes).digest("hex");
|
|
2788
2817
|
}
|
|
2789
|
-
function privateMode(st) {
|
|
2790
|
-
if (process.platform !== "win32" && ((st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
2818
|
+
function privateMode(st, ownerOnly = true) {
|
|
2819
|
+
if (process.platform !== "win32" && (ownerOnly && (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
2791
2820
|
refused();
|
|
2792
2821
|
}
|
|
2793
2822
|
function same(a, b) {
|
|
@@ -2822,7 +2851,7 @@ class DirectoryWalks {
|
|
|
2822
2851
|
if (this.open())
|
|
2823
2852
|
this.walked.set(path, { dev: st.dev, ino: st.ino });
|
|
2824
2853
|
}
|
|
2825
|
-
reuse(path) {
|
|
2854
|
+
reuse(path, ownerOnly = true) {
|
|
2826
2855
|
const walked = this.open() ? this.walked.get(path) : undefined;
|
|
2827
2856
|
if (!walked)
|
|
2828
2857
|
return false;
|
|
@@ -2839,15 +2868,17 @@ class DirectoryWalks {
|
|
|
2839
2868
|
this.walked.delete(path);
|
|
2840
2869
|
return false;
|
|
2841
2870
|
}
|
|
2842
|
-
privateMode(st);
|
|
2871
|
+
privateMode(st, ownerOnly);
|
|
2843
2872
|
return true;
|
|
2844
2873
|
}
|
|
2845
2874
|
}
|
|
2846
2875
|
|
|
2847
2876
|
class PrivateFiles {
|
|
2848
2877
|
namespace;
|
|
2849
|
-
|
|
2878
|
+
ownerOnly;
|
|
2879
|
+
constructor(namespace, ownerOnly = true) {
|
|
2850
2880
|
this.namespace = namespace;
|
|
2881
|
+
this.ownerOnly = ownerOnly;
|
|
2851
2882
|
}
|
|
2852
2883
|
directory(path, create = false, walks) {
|
|
2853
2884
|
const tail = relative2(this.namespace, path);
|
|
@@ -2855,7 +2886,7 @@ class PrivateFiles {
|
|
|
2855
2886
|
refused();
|
|
2856
2887
|
if (!create) {
|
|
2857
2888
|
const full = resolve3(path);
|
|
2858
|
-
if (walks?.reuse(full))
|
|
2889
|
+
if (walks?.reuse(full, this.ownerOnly))
|
|
2859
2890
|
return true;
|
|
2860
2891
|
const namespace = resolve3(this.namespace);
|
|
2861
2892
|
let inside = false, last;
|
|
@@ -2863,7 +2894,7 @@ class PrivateFiles {
|
|
|
2863
2894
|
if (at2 === namespace)
|
|
2864
2895
|
inside = true;
|
|
2865
2896
|
if (inside)
|
|
2866
|
-
privateMode(st);
|
|
2897
|
+
privateMode(st, this.ownerOnly);
|
|
2867
2898
|
last = st;
|
|
2868
2899
|
});
|
|
2869
2900
|
if (found && last)
|
|
@@ -2872,7 +2903,7 @@ class PrivateFiles {
|
|
|
2872
2903
|
}
|
|
2873
2904
|
if (!safeDirectory(this.namespace, true))
|
|
2874
2905
|
return false;
|
|
2875
|
-
privateMode(lstatSync3(this.namespace));
|
|
2906
|
+
privateMode(lstatSync3(this.namespace), this.ownerOnly);
|
|
2876
2907
|
let at = this.namespace;
|
|
2877
2908
|
for (const part of tail.split(sep3).filter(Boolean)) {
|
|
2878
2909
|
at = join3(at, part);
|
|
@@ -2892,7 +2923,7 @@ class PrivateFiles {
|
|
|
2892
2923
|
}
|
|
2893
2924
|
if (!st.isDirectory() || st.isSymbolicLink())
|
|
2894
2925
|
throw new Error("Unsafe directory");
|
|
2895
|
-
privateMode(st);
|
|
2926
|
+
privateMode(st, this.ownerOnly);
|
|
2896
2927
|
}
|
|
2897
2928
|
return true;
|
|
2898
2929
|
}
|
|
@@ -2909,7 +2940,7 @@ class PrivateFiles {
|
|
|
2909
2940
|
}
|
|
2910
2941
|
if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
|
|
2911
2942
|
refused();
|
|
2912
|
-
privateMode(before);
|
|
2943
|
+
privateMode(before, this.ownerOnly);
|
|
2913
2944
|
const fd = openSync2(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
2914
2945
|
try {
|
|
2915
2946
|
const opened = fstatSync2(fd);
|
|
@@ -5843,13 +5874,13 @@ function startupGuidance(result, input) {
|
|
|
5843
5874
|
return `Cadence refused its installed package files (web/dist or agents); reinstall the package rather than editing it.
|
|
5844
5875
|
`;
|
|
5845
5876
|
case "pi-agent-directory":
|
|
5846
|
-
return `Pi's agent directory ${agent} must be an existing real directory owned by you
|
|
5877
|
+
return `Pi's agent directory ${agent} must be an existing real directory (not a symlink) owned by you.
|
|
5847
5878
|
`;
|
|
5848
5879
|
case "pi-auth":
|
|
5849
|
-
return `${join8(agent, "auth.json")} must be your own single-link regular file
|
|
5880
|
+
return `${join8(agent, "auth.json")} must be your own single-link regular file holding valid Pi credentials; log in to Pi first.
|
|
5850
5881
|
`;
|
|
5851
5882
|
case "pi-models":
|
|
5852
|
-
return `${join8(agent, "models.json")} must be your own single-link regular file
|
|
5883
|
+
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
5884
|
`;
|
|
5854
5885
|
case "pi-runtime":
|
|
5855
5886
|
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 +6015,30 @@ function autostartGuidance(s, result) {
|
|
|
5984
6015
|
` : "";
|
|
5985
6016
|
return note + autostartStatusGuidance(s, result, name, cli, workspace, manage);
|
|
5986
6017
|
}
|
|
6018
|
+
function lingerGuidance(linger) {
|
|
6019
|
+
switch (linger) {
|
|
6020
|
+
case "enabled":
|
|
6021
|
+
return `Linger enabled: Cadence starts at boot and keeps running after logout (undo: loginctl disable-linger).
|
|
6022
|
+
`;
|
|
6023
|
+
case "off":
|
|
6024
|
+
return `Linger is off: Cadence starts at login and stops at logout. Start it at boot: loginctl enable-linger
|
|
6025
|
+
`;
|
|
6026
|
+
case "refused":
|
|
6027
|
+
return `Linger needs authorization: Cadence starts at login and stops at logout. Start it at boot: loginctl enable-linger (may ask for your password)
|
|
6028
|
+
`;
|
|
6029
|
+
default:
|
|
6030
|
+
return "";
|
|
6031
|
+
}
|
|
6032
|
+
}
|
|
5987
6033
|
function autostartStatusGuidance(s, result, name, cli, workspace, manage) {
|
|
5988
6034
|
switch (result.status) {
|
|
5989
6035
|
case "started": {
|
|
6036
|
+
const linger = lingerGuidance(result.linger);
|
|
5990
6037
|
if (result.service !== "alive")
|
|
5991
6038
|
return `${s.unit} started for ${result.workspace}, but the owner reports ${result.service}.
|
|
5992
6039
|
Inspect: node ${cli} diagnose --workspace ${workspace}
|
|
5993
6040
|
${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" : "";
|
|
6041
|
+
${linger}`;
|
|
5996
6042
|
return `Cadence WebUI: ${result.endpoint} (${s.unit}, workspace ${result.workspace})
|
|
5997
6043
|
Enter with your private link: node ${cli} token-link --workspace ${workspace} --reveal
|
|
5998
6044
|
${manage}
|
|
@@ -6001,7 +6047,7 @@ ${linger}`;
|
|
|
6001
6047
|
case "waiting-for-pi-login":
|
|
6002
6048
|
return `${s.unit} is enabled for ${result.workspace}; it starts once Pi has ${join8(agentDirectory(s), "auth.json")}.
|
|
6003
6049
|
Log in to Pi, then: systemctl --user start ${name}
|
|
6004
|
-
`;
|
|
6050
|
+
${lingerGuidance(result.linger)}`;
|
|
6005
6051
|
case "disabled":
|
|
6006
6052
|
return `${s.unit} refreshed for ${result.workspace}; left disabled as configured.
|
|
6007
6053
|
`;
|
|
@@ -6019,7 +6065,7 @@ Log in to Pi, then: systemctl --user start ${name}
|
|
|
6019
6065
|
`;
|
|
6020
6066
|
case "failed":
|
|
6021
6067
|
return `Could not enable or start ${s.unit}; see systemctl --user status ${name} and journalctl --user -u ${name}.
|
|
6022
|
-
`;
|
|
6068
|
+
${lingerGuidance(result.linger)}`;
|
|
6023
6069
|
default:
|
|
6024
6070
|
return "";
|
|
6025
6071
|
}
|
|
@@ -6027,8 +6073,16 @@ Log in to Pi, then: systemctl --user start ${name}
|
|
|
6027
6073
|
async function autostartMain(argv, hostUrl) {
|
|
6028
6074
|
const action = argv.shift();
|
|
6029
6075
|
let requested;
|
|
6030
|
-
|
|
6031
|
-
|
|
6076
|
+
let linger = false;
|
|
6077
|
+
while (action === "install" && argv.length) {
|
|
6078
|
+
if (argv[0] === "--workspace" && argv[1] && requested === undefined)
|
|
6079
|
+
requested = resolve5(argv.splice(0, 2)[1]);
|
|
6080
|
+
else if (argv[0] === "--linger" && !linger) {
|
|
6081
|
+
argv.shift();
|
|
6082
|
+
linger = true;
|
|
6083
|
+
} else
|
|
6084
|
+
break;
|
|
6085
|
+
}
|
|
6032
6086
|
if (argv.length || !["install", "refresh", "remove", "status"].includes(action ?? ""))
|
|
6033
6087
|
throw new ContractError("invalid");
|
|
6034
6088
|
const s = hostSystem(dirname8(dirname8(fileURLToPath(hostUrl))));
|
|
@@ -6039,7 +6093,7 @@ async function autostartMain(argv, hostUrl) {
|
|
|
6039
6093
|
result = autostartStatus(s);
|
|
6040
6094
|
else {
|
|
6041
6095
|
try {
|
|
6042
|
-
result = configureAutostart(s, action === "install" ? "install" : "refresh", requested);
|
|
6096
|
+
result = configureAutostart(s, action === "install" ? "install" : "refresh", requested, linger);
|
|
6043
6097
|
} catch (error) {
|
|
6044
6098
|
if (error instanceof ContractError && error.code === "invalid")
|
|
6045
6099
|
process.stderr.write(`Cadence autostart needs an existing absolute workspace directory (--workspace or CADENCE_WORKSPACE).
|
|
@@ -6062,8 +6116,8 @@ async function managementMain(argv, hostUrl) {
|
|
|
6062
6116
|
const command = argv.shift();
|
|
6063
6117
|
if (command === "--help" && !argv.length) {
|
|
6064
6118
|
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.
|
|
6119
|
+
cadence-service autostart install [--workspace PATH] [--linger]|refresh|remove|status
|
|
6120
|
+
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
6121
|
Return/quit detaches clients; only explicit Cancel/Stop requests stop. Unconfirmed/quarantined is not stopped.
|
|
6068
6122
|
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
6123
|
`);
|
package/src/service-host.mjs
CHANGED
|
@@ -2134,8 +2134,8 @@ function refused() {
|
|
|
2134
2134
|
function hash(bytes) {
|
|
2135
2135
|
return createHash3("sha256").update(bytes).digest("hex");
|
|
2136
2136
|
}
|
|
2137
|
-
function privateMode(st) {
|
|
2138
|
-
if (process.platform !== "win32" && ((st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
2137
|
+
function privateMode(st, ownerOnly = true) {
|
|
2138
|
+
if (process.platform !== "win32" && (ownerOnly && (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid()))
|
|
2139
2139
|
refused();
|
|
2140
2140
|
}
|
|
2141
2141
|
function same(a, b) {
|
|
@@ -2170,7 +2170,7 @@ class DirectoryWalks {
|
|
|
2170
2170
|
if (this.open())
|
|
2171
2171
|
this.walked.set(path, { dev: st.dev, ino: st.ino });
|
|
2172
2172
|
}
|
|
2173
|
-
reuse(path) {
|
|
2173
|
+
reuse(path, ownerOnly = true) {
|
|
2174
2174
|
const walked = this.open() ? this.walked.get(path) : undefined;
|
|
2175
2175
|
if (!walked)
|
|
2176
2176
|
return false;
|
|
@@ -2187,15 +2187,17 @@ class DirectoryWalks {
|
|
|
2187
2187
|
this.walked.delete(path);
|
|
2188
2188
|
return false;
|
|
2189
2189
|
}
|
|
2190
|
-
privateMode(st);
|
|
2190
|
+
privateMode(st, ownerOnly);
|
|
2191
2191
|
return true;
|
|
2192
2192
|
}
|
|
2193
2193
|
}
|
|
2194
2194
|
|
|
2195
2195
|
class PrivateFiles {
|
|
2196
2196
|
namespace;
|
|
2197
|
-
|
|
2197
|
+
ownerOnly;
|
|
2198
|
+
constructor(namespace, ownerOnly = true) {
|
|
2198
2199
|
this.namespace = namespace;
|
|
2200
|
+
this.ownerOnly = ownerOnly;
|
|
2199
2201
|
}
|
|
2200
2202
|
directory(path, create = false, walks) {
|
|
2201
2203
|
const tail = relative2(this.namespace, path);
|
|
@@ -2203,7 +2205,7 @@ class PrivateFiles {
|
|
|
2203
2205
|
refused();
|
|
2204
2206
|
if (!create) {
|
|
2205
2207
|
const full = resolve2(path);
|
|
2206
|
-
if (walks?.reuse(full))
|
|
2208
|
+
if (walks?.reuse(full, this.ownerOnly))
|
|
2207
2209
|
return true;
|
|
2208
2210
|
const namespace = resolve2(this.namespace);
|
|
2209
2211
|
let inside = false, last;
|
|
@@ -2211,7 +2213,7 @@ class PrivateFiles {
|
|
|
2211
2213
|
if (at2 === namespace)
|
|
2212
2214
|
inside = true;
|
|
2213
2215
|
if (inside)
|
|
2214
|
-
privateMode(st);
|
|
2216
|
+
privateMode(st, this.ownerOnly);
|
|
2215
2217
|
last = st;
|
|
2216
2218
|
});
|
|
2217
2219
|
if (found && last)
|
|
@@ -2220,7 +2222,7 @@ class PrivateFiles {
|
|
|
2220
2222
|
}
|
|
2221
2223
|
if (!safeDirectory(this.namespace, true))
|
|
2222
2224
|
return false;
|
|
2223
|
-
privateMode(lstatSync2(this.namespace));
|
|
2225
|
+
privateMode(lstatSync2(this.namespace), this.ownerOnly);
|
|
2224
2226
|
let at = this.namespace;
|
|
2225
2227
|
for (const part of tail.split(sep2).filter(Boolean)) {
|
|
2226
2228
|
at = join2(at, part);
|
|
@@ -2240,7 +2242,7 @@ class PrivateFiles {
|
|
|
2240
2242
|
}
|
|
2241
2243
|
if (!st.isDirectory() || st.isSymbolicLink())
|
|
2242
2244
|
throw new Error("Unsafe directory");
|
|
2243
|
-
privateMode(st);
|
|
2245
|
+
privateMode(st, this.ownerOnly);
|
|
2244
2246
|
}
|
|
2245
2247
|
return true;
|
|
2246
2248
|
}
|
|
@@ -2257,7 +2259,7 @@ class PrivateFiles {
|
|
|
2257
2259
|
}
|
|
2258
2260
|
if (!before.isFile() || before.nlink !== 1 || before.size > maximum)
|
|
2259
2261
|
refused();
|
|
2260
|
-
privateMode(before);
|
|
2262
|
+
privateMode(before, this.ownerOnly);
|
|
2261
2263
|
const fd = openSync2(path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
2262
2264
|
try {
|
|
2263
2265
|
const opened = fstatSync2(fd);
|
|
@@ -17108,7 +17110,7 @@ function piSource(agentDir, home, cwd, env) {
|
|
|
17108
17110
|
const directory = join12(realpathSync4(dirname9(selected2)), selected2.slice(dirname9(selected2).length + 1));
|
|
17109
17111
|
if (!lstatSync8(directory).isDirectory() || lstatSync8(directory).isSymbolicLink())
|
|
17110
17112
|
refuse2();
|
|
17111
|
-
const files = new PrivateFiles(directory);
|
|
17113
|
+
const files = new PrivateFiles(directory, false);
|
|
17112
17114
|
if (!files.directory(directory))
|
|
17113
17115
|
refuse2();
|
|
17114
17116
|
return {
|
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));
|