@tangle-network/agent-profile-materialize 0.26.2 → 0.26.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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/managed-workspace-stage.d.ts +1 -1
- package/dist/plan-application.js +10 -7
- package/dist/plan-types.d.ts +6 -7
- package/dist/secret-concurrent-revision.d.ts +1 -0
- package/dist/secret-concurrent-revision.js +15 -10
- package/dist/secret-session-view.d.ts +30 -0
- package/dist/secret-session-view.js +136 -0
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -26,3 +26,4 @@ export * from "./profile-start-check.js";
|
|
|
26
26
|
export type { ResolveAgentProfileResourcesOptions } from "./resource-resolution.js";
|
|
27
27
|
export { normalizeSkillMd, resolveAgentProfileResources, } from "./resource-resolution.js";
|
|
28
28
|
export { prepareSecretConcurrentProfileRevision } from "./secret-concurrent-revision.js";
|
|
29
|
+
export * from "./secret-session-view.js";
|
package/dist/index.js
CHANGED
|
@@ -20,3 +20,4 @@ export { executionControlConflict, materializeProfile, UNAVAILABLE_AGENT_PROFILE
|
|
|
20
20
|
export * from "./profile-start-check.js";
|
|
21
21
|
export { normalizeSkillMd, resolveAgentProfileResources, } from "./resource-resolution.js";
|
|
22
22
|
export { prepareSecretConcurrentProfileRevision } from "./secret-concurrent-revision.js";
|
|
23
|
+
export * from "./secret-session-view.js";
|
|
@@ -20,5 +20,5 @@ export declare function assertPrivateManagedRoot(root: string): void;
|
|
|
20
20
|
* The caller must keep the returned tree while an admitted turn uses it and
|
|
21
21
|
* dispose it after that turn, so a later binding cannot expose old secrets.
|
|
22
22
|
*/
|
|
23
|
-
export declare function prepareManagedWorkspacePlan(plan: WorkspacePlan, managedRoot: string, sharedWorkspaceDir: string, secretProvider: AgentProfileSecretProvider, options?: Pick<ApplyWorkspacePlanOptions, "ownership" | "runtimeOnlyFiles" | "
|
|
23
|
+
export declare function prepareManagedWorkspacePlan(plan: WorkspacePlan, managedRoot: string, sharedWorkspaceDir: string, secretProvider: AgentProfileSecretProvider, options?: Pick<ApplyWorkspacePlanOptions, "ownership" | "runtimeOnlyFiles" | "secretFileSlotsByReference">): Promise<PreparedManagedWorkspace>;
|
|
24
24
|
export declare function lstatIfPresent(path: string): ReturnType<typeof lstatSync> | null;
|
package/dist/plan-application.js
CHANGED
|
@@ -9,7 +9,7 @@ export function applyWorkspacePlan(plan, workspaceDir, options = {}) {
|
|
|
9
9
|
*/
|
|
10
10
|
export async function applyWorkspacePlanWithSecretProvider(plan, workspaceDir, secretProvider, options = {}) {
|
|
11
11
|
const planDigest = hashWorkspacePlan(plan);
|
|
12
|
-
const resolved = await resolvePrivateWorkspacePlan(plan, secretProvider, options.
|
|
12
|
+
const resolved = await resolvePrivateWorkspacePlan(plan, secretProvider, options.secretFileSlotsByReference === true);
|
|
13
13
|
return applyResolvedWorkspacePlan(plan, resolved, workspaceDir, options, planDigest);
|
|
14
14
|
}
|
|
15
15
|
function secretRefLabel(ref) {
|
|
@@ -81,7 +81,7 @@ function resolvePublicWorkspacePlan(plan) {
|
|
|
81
81
|
/**
|
|
82
82
|
* OpenCode substitutes `{env:NAME}` textually into its config before parsing
|
|
83
83
|
* it, so only a value that cannot break out of a JSON string or start another
|
|
84
|
-
* substitution may
|
|
84
|
+
* substitution may stand behind a reference.
|
|
85
85
|
*/
|
|
86
86
|
const SUBSTITUTABLE_SECRET_VALUE = /^[\x20-\x7e]+$/;
|
|
87
87
|
const UNSUBSTITUTABLE_SECRET_CHARACTERS = /["\\{}]/;
|
|
@@ -93,7 +93,7 @@ function secretSlotEnvironmentName(ref) {
|
|
|
93
93
|
.toUpperCase();
|
|
94
94
|
return `TANGLE_PROFILE_SECRET_${digest}`;
|
|
95
95
|
}
|
|
96
|
-
async function resolvePrivateWorkspacePlan(plan, secretProvider,
|
|
96
|
+
async function resolvePrivateWorkspacePlan(plan, secretProvider, fileSlotsByReference) {
|
|
97
97
|
if (!secretProvider || typeof secretProvider.get !== "function") {
|
|
98
98
|
throw new Error("AgentProfileSecretProvider.get is required");
|
|
99
99
|
}
|
|
@@ -123,13 +123,16 @@ async function resolvePrivateWorkspacePlan(plan, secretProvider, fileSlotEnviron
|
|
|
123
123
|
const resolveFileSlot = async (input) => {
|
|
124
124
|
const ref = agentProfileSecretRefSchema.parse(input);
|
|
125
125
|
const value = await resolveRawRef(ref);
|
|
126
|
-
if (!
|
|
127
|
-
!SUBSTITUTABLE_SECRET_VALUE.test(value) ||
|
|
128
|
-
UNSUBSTITUTABLE_SECRET_CHARACTERS.test(value)) {
|
|
126
|
+
if (!fileSlotsByReference) {
|
|
129
127
|
return ref.format === "bearer" ? `Bearer ${value}` : value;
|
|
130
128
|
}
|
|
129
|
+
// Never write the value in place: the file is readable by the process
|
|
130
|
+
// that loads it, and so by every tool that process runs.
|
|
131
|
+
if (!SUBSTITUTABLE_SECRET_VALUE.test(value) ||
|
|
132
|
+
UNSUBSTITUTABLE_SECRET_CHARACTERS.test(value)) {
|
|
133
|
+
throw new Error(`AgentProfile secret ${secretRefLabel(ref)} has a value that cannot be referenced from a secret OpenCode config: it may hold only printable ASCII without quotes, backslashes or braces`);
|
|
134
|
+
}
|
|
131
135
|
const name = secretSlotEnvironmentName(ref);
|
|
132
|
-
fileSlotEnvironment[name] = value;
|
|
133
136
|
return ref.format === "bearer" ? `Bearer {env:${name}}` : `{env:${name}}`;
|
|
134
137
|
};
|
|
135
138
|
const files = await Promise.all(normalizePlanFiles(plan.files).map(async (file) => {
|
package/dist/plan-types.d.ts
CHANGED
|
@@ -321,14 +321,13 @@ export interface ApplyWorkspacePlanOptions {
|
|
|
321
321
|
*/
|
|
322
322
|
appliedPlanDigest?: string;
|
|
323
323
|
/**
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
* entries to the launch environment after digesting the launch.
|
|
324
|
+
* Write every JSON file secret slot as an `{env:NAME}` reference (OpenCode
|
|
325
|
+
* config substitution) and never its value, so file bytes and every digest
|
|
326
|
+
* over them stay identical when only a secret's value changes. The value is
|
|
327
|
+
* not returned: a native process that held it would hand it to every tool
|
|
328
|
+
* it runs. A value that could not be substituted safely is refused.
|
|
330
329
|
*/
|
|
331
|
-
|
|
330
|
+
secretFileSlotsByReference?: true;
|
|
332
331
|
}
|
|
333
332
|
import type { AgentProfileSecretRef } from "@tangle-network/agent-interface";
|
|
334
333
|
import type { HarnessId } from "./harness.js";
|
|
@@ -19,3 +19,4 @@ export declare function prepareSecretConcurrentProfileRevision(input: {
|
|
|
19
19
|
secretProvider: AgentProfileSecretProvider;
|
|
20
20
|
options?: Pick<ApplyWorkspacePlanOptions, "runtimeOnlyFiles">;
|
|
21
21
|
}): Promise<ConcurrentProfileRevision>;
|
|
22
|
+
export declare function runSetfacl(args: string[], paths: readonly string[]): void;
|
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { assertLaunchRoot, listRevisionFiles, makeStageWritable, prepareExplicitOpenCodePlan, } from "./managed-workspace.js";
|
|
6
6
|
import { bindConcurrentLaunch } from "./managed-workspace-bind.js";
|
|
7
7
|
import { lstatIfPresent, prepareManagedWorkspacePlan, } from "./managed-workspace-stage.js";
|
|
8
|
+
import { normalizeWorkspacePlanArgument } from "./plan-normalize.js";
|
|
8
9
|
import { assertWorkspacePlanSupported } from "./plan-support.js";
|
|
9
10
|
/**
|
|
10
11
|
* Publish a resolved revision for exactly one native subprocess UID. The
|
|
@@ -52,12 +53,20 @@ export async function prepareSecretConcurrentProfileRevision(input) {
|
|
|
52
53
|
if (plan.files.some((file) => file.relPath.startsWith(".config/opencode/"))) {
|
|
53
54
|
throw new Error("Secret OpenCode reserves its native config directory");
|
|
54
55
|
}
|
|
56
|
+
// OpenCode and every process it spawns run as the session UID, so a value in
|
|
57
|
+
// its environment, argv or config is readable by the agent's own shell
|
|
58
|
+
// (`/proc/<pid>/environ`, `/proc/<pid>/cmdline`, `cat`). No profile secret
|
|
59
|
+
// value may reach the launch: config slots stay references whose values the
|
|
60
|
+
// launch never receives (#11875).
|
|
61
|
+
if (Object.values(plan.env).some((value) => typeof value !== "string") ||
|
|
62
|
+
plan.flags.some((argument) => typeof normalizeWorkspacePlanArgument(argument) !== "string")) {
|
|
63
|
+
throw new Error("Secret OpenCode refuses a profile secret in its launch environment or arguments");
|
|
64
|
+
}
|
|
55
65
|
assertWorkspacePlanSupported(plan);
|
|
56
66
|
assertLaunchRoot(input.launchRoot);
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
const
|
|
60
|
-
const prepared = await prepareManagedWorkspacePlan(plan, input.managedRoot, input.sharedWorkspaceDir, input.secretProvider, { ...input.options, secretFileSlotEnvironment: secretEnvironment });
|
|
67
|
+
// File slots reference their secrets, so an unchanged profile with a rotated
|
|
68
|
+
// per-turn secret reuses this revision.
|
|
69
|
+
const prepared = await prepareManagedWorkspacePlan(plan, input.managedRoot, input.sharedWorkspaceDir, input.secretProvider, { ...input.options, secretFileSlotsByReference: true });
|
|
61
70
|
try {
|
|
62
71
|
// XDG_CONFIG_HOME is bound to this executor-owned tree at native launch.
|
|
63
72
|
// An agent-owned HOME may hold SQLite state, never native authority.
|
|
@@ -112,14 +121,10 @@ export async function prepareSecretConcurrentProfileRevision(input) {
|
|
|
112
121
|
prepared.dispose();
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
|
-
// Added after launchDigest so the digest and receipt never cover a value.
|
|
116
124
|
return {
|
|
117
125
|
directory,
|
|
118
126
|
manifest: prepared.manifest,
|
|
119
|
-
application: {
|
|
120
|
-
...prepared.application,
|
|
121
|
-
spawn: { ...spawn, env: { ...spawn.env, ...secretEnvironment } },
|
|
122
|
-
},
|
|
127
|
+
application: { ...prepared.application, spawn },
|
|
123
128
|
};
|
|
124
129
|
}
|
|
125
130
|
catch (error) {
|
|
@@ -154,7 +159,7 @@ function secretRevisionPaths(root) {
|
|
|
154
159
|
walk(root);
|
|
155
160
|
return { all, exposed };
|
|
156
161
|
}
|
|
157
|
-
function runSetfacl(args, paths) {
|
|
162
|
+
export function runSetfacl(args, paths) {
|
|
158
163
|
for (let start = 0; start < paths.length; start += 64) {
|
|
159
164
|
execFileSync("/usr/bin/setfacl", [...args, "--", ...paths.slice(start, start + 64)], {
|
|
160
165
|
env: { PATH: "/usr/bin:/bin", LANG: "C" },
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stable, sealed cwd of one secret OpenCode session. OpenCode reopens a
|
|
3
|
+
* native session at the directory it was created in, so a session resumed on
|
|
4
|
+
* a later turn keeps this path while each turn's revision changes. The name
|
|
5
|
+
* binds the shared workspace, the session and its subprocess UID, so two
|
|
6
|
+
* sessions never share a view.
|
|
7
|
+
*/
|
|
8
|
+
export declare function secretSessionViewWorkspace(input: {
|
|
9
|
+
launchRoot: string;
|
|
10
|
+
sharedWorkspaceDir: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
subprocessUid: number;
|
|
13
|
+
}): string;
|
|
14
|
+
/**
|
|
15
|
+
* Point a session's view at one published secret revision before the
|
|
16
|
+
* session's turn runs. The view's `reference/` becomes a fresh tree of hard
|
|
17
|
+
* links to the revision's read-only files (the same inodes, owner, mode and
|
|
18
|
+
* ACL), so the session reads this turn's files and nothing the previous
|
|
19
|
+
* revision held. The view holds nothing else, so OpenCode's config search
|
|
20
|
+
* finds no config there; profile config stays bound by absolute path.
|
|
21
|
+
*
|
|
22
|
+
* The swap is not atomic for a reader that is already running: the caller
|
|
23
|
+
* runs one turn of a session at a time and binds before that turn spawns.
|
|
24
|
+
*/
|
|
25
|
+
export declare function bindSecretSessionView(input: {
|
|
26
|
+
launchRoot: string;
|
|
27
|
+
viewWorkspaceDir: string;
|
|
28
|
+
revisionWorkspaceDir: string;
|
|
29
|
+
subprocessUid: number;
|
|
30
|
+
}): void;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { chmodSync, linkSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { assertLaunchRoot } from "./managed-workspace.js";
|
|
5
|
+
import { runSetfacl } from "./secret-concurrent-revision.js";
|
|
6
|
+
/** The only cwd-relative content a secret OpenCode revision serves. */
|
|
7
|
+
const VIEW_ENTRY = "reference";
|
|
8
|
+
/**
|
|
9
|
+
* The stable, sealed cwd of one secret OpenCode session. OpenCode reopens a
|
|
10
|
+
* native session at the directory it was created in, so a session resumed on
|
|
11
|
+
* a later turn keeps this path while each turn's revision changes. The name
|
|
12
|
+
* binds the shared workspace, the session and its subprocess UID, so two
|
|
13
|
+
* sessions never share a view.
|
|
14
|
+
*/
|
|
15
|
+
export function secretSessionViewWorkspace(input) {
|
|
16
|
+
if (!input.sessionId)
|
|
17
|
+
throw new Error("Secret session view needs a session");
|
|
18
|
+
const name = createHash("sha256")
|
|
19
|
+
.update("tangle-secret-session-view\0")
|
|
20
|
+
.update(input.sharedWorkspaceDir)
|
|
21
|
+
.update("\0")
|
|
22
|
+
.update(input.sessionId)
|
|
23
|
+
.update("\0")
|
|
24
|
+
.update(String(input.subprocessUid))
|
|
25
|
+
.digest("hex");
|
|
26
|
+
return join(input.launchRoot, `session-${name}`, "workspace");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Point a session's view at one published secret revision before the
|
|
30
|
+
* session's turn runs. The view's `reference/` becomes a fresh tree of hard
|
|
31
|
+
* links to the revision's read-only files (the same inodes, owner, mode and
|
|
32
|
+
* ACL), so the session reads this turn's files and nothing the previous
|
|
33
|
+
* revision held. The view holds nothing else, so OpenCode's config search
|
|
34
|
+
* finds no config there; profile config stays bound by absolute path.
|
|
35
|
+
*
|
|
36
|
+
* The swap is not atomic for a reader that is already running: the caller
|
|
37
|
+
* runs one turn of a session at a time and binds before that turn spawns.
|
|
38
|
+
*/
|
|
39
|
+
export function bindSecretSessionView(input) {
|
|
40
|
+
const { launchRoot, viewWorkspaceDir, revisionWorkspaceDir } = input;
|
|
41
|
+
if (process.platform !== "linux" ||
|
|
42
|
+
process.getuid?.() !== 0 ||
|
|
43
|
+
!Number.isSafeInteger(input.subprocessUid) ||
|
|
44
|
+
input.subprocessUid < 200_000) {
|
|
45
|
+
throw new Error("Secret session view needs a root executor and reserved identity");
|
|
46
|
+
}
|
|
47
|
+
assertLaunchRoot(launchRoot);
|
|
48
|
+
const viewRoot = dirname(viewWorkspaceDir);
|
|
49
|
+
if (!isLaunchWorkspace(launchRoot, viewWorkspaceDir, /^session-[a-f0-9]{64}$/)) {
|
|
50
|
+
throw new Error("Secret session view is not a launch-root session path");
|
|
51
|
+
}
|
|
52
|
+
if (!isLaunchWorkspace(launchRoot, revisionWorkspaceDir, /^revision-[a-f0-9]{64}$/) ||
|
|
53
|
+
!isSealedNode(lstatSync(revisionWorkspaceDir), "directory")) {
|
|
54
|
+
throw new Error("Secret session view needs a published revision");
|
|
55
|
+
}
|
|
56
|
+
for (const directory of [viewRoot, viewWorkspaceDir]) {
|
|
57
|
+
const stat = lstatIfPresent(directory);
|
|
58
|
+
if (!stat)
|
|
59
|
+
mkdirSync(directory, { mode: 0o700 });
|
|
60
|
+
else if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== 0) {
|
|
61
|
+
throw new Error("Secret session view is not an executor-owned directory");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (readdirSync(viewWorkspaceDir).some((name) => name !== VIEW_ENTRY)) {
|
|
65
|
+
throw new Error("Secret session view holds an unexpected entry");
|
|
66
|
+
}
|
|
67
|
+
// Only this function writes a view. An interrupted bind leaves a staged or
|
|
68
|
+
// retired tree beside `workspace`, never inside it.
|
|
69
|
+
clearBesideWorkspace(viewRoot);
|
|
70
|
+
const source = join(revisionWorkspaceDir, VIEW_ENTRY);
|
|
71
|
+
const sourceStat = lstatIfPresent(source);
|
|
72
|
+
let staged;
|
|
73
|
+
if (sourceStat) {
|
|
74
|
+
if (!isSealedNode(sourceStat, "directory")) {
|
|
75
|
+
throw new Error("Secret revision reference/ is not sealed");
|
|
76
|
+
}
|
|
77
|
+
staged = join(viewRoot, `.stage-${randomBytes(8).toString("hex")}`);
|
|
78
|
+
const directories = linkSealedTree(source, staged);
|
|
79
|
+
sealDirectories(directories, input.subprocessUid);
|
|
80
|
+
}
|
|
81
|
+
const current = join(viewWorkspaceDir, VIEW_ENTRY);
|
|
82
|
+
if (lstatIfPresent(current)) {
|
|
83
|
+
renameSync(current, join(viewRoot, `.retired-${randomBytes(8).toString("hex")}`));
|
|
84
|
+
}
|
|
85
|
+
if (staged)
|
|
86
|
+
renameSync(staged, current);
|
|
87
|
+
clearBesideWorkspace(viewRoot);
|
|
88
|
+
sealDirectories([viewRoot, viewWorkspaceDir], input.subprocessUid);
|
|
89
|
+
}
|
|
90
|
+
function isLaunchWorkspace(launchRoot, workspace, name) {
|
|
91
|
+
return (basename(workspace) === "workspace" &&
|
|
92
|
+
dirname(dirname(workspace)) === launchRoot &&
|
|
93
|
+
name.test(basename(dirname(workspace))));
|
|
94
|
+
}
|
|
95
|
+
function lstatIfPresent(path) {
|
|
96
|
+
return lstatSync(path, { throwIfNoEntry: false });
|
|
97
|
+
}
|
|
98
|
+
function isSealedNode(stat, kind) {
|
|
99
|
+
return (!stat.isSymbolicLink() &&
|
|
100
|
+
(kind === "directory" ? stat.isDirectory() : stat.isFile()) &&
|
|
101
|
+
stat.uid === 0 &&
|
|
102
|
+
(stat.mode & 0o222) === 0);
|
|
103
|
+
}
|
|
104
|
+
/** Hard-link a sealed revision tree; returns every directory it created. */
|
|
105
|
+
function linkSealedTree(source, target) {
|
|
106
|
+
mkdirSync(target, { mode: 0o700 });
|
|
107
|
+
const directories = [target];
|
|
108
|
+
for (const name of readdirSync(source)) {
|
|
109
|
+
const from = join(source, name);
|
|
110
|
+
const stat = lstatSync(from);
|
|
111
|
+
if (isSealedNode(stat, "directory")) {
|
|
112
|
+
directories.push(...linkSealedTree(from, join(target, name)));
|
|
113
|
+
}
|
|
114
|
+
else if (isSealedNode(stat, "file")) {
|
|
115
|
+
linkSync(from, join(target, name));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
throw new Error("Secret revision reference/ holds an unsealed inode");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return directories;
|
|
122
|
+
}
|
|
123
|
+
/** Root-owned, read-only, and traversable only by the session's UID. */
|
|
124
|
+
function sealDirectories(directories, uid) {
|
|
125
|
+
runSetfacl(["-b"], directories);
|
|
126
|
+
for (const directory of directories)
|
|
127
|
+
chmodSync(directory, 0o500);
|
|
128
|
+
runSetfacl(["-m", `u:${uid}:r-X`], directories);
|
|
129
|
+
}
|
|
130
|
+
function clearBesideWorkspace(viewRoot) {
|
|
131
|
+
for (const name of readdirSync(viewRoot)) {
|
|
132
|
+
if (name !== "workspace") {
|
|
133
|
+
rmSync(join(viewRoot, name), { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-profile-materialize",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.4",
|
|
4
4
|
"description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-container, and VB).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"@tangle-network/agent-interface": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
|
36
36
|
},
|
|
37
37
|
"tangleRelease": {
|
|
38
|
-
"sourceSha": "
|
|
39
|
-
"mergedAt": "2026-10-
|
|
38
|
+
"sourceSha": "7375b922752bb85e4e7541a519be661c363b115f",
|
|
39
|
+
"mergedAt": "2026-10-19T05:43:15-06:00"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc -p tsconfig.json",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"test": "pnpm run test:installed-peer && pnpm run test:current-peer",
|
|
46
46
|
"test:installed-peer": "vitest run",
|
|
47
47
|
"test:current-peer": "vitest run --config vitest.current-peer.config.ts",
|
|
48
|
-
"test:root": "node ../../scripts/ci/vitest-as-root.mjs tests/concurrent-subagents.test.ts tests/secret-provider-revision.test.ts tests/secret-reference-resources.test.ts",
|
|
48
|
+
"test:root": "node ../../scripts/ci/vitest-as-root.mjs tests/concurrent-subagents.test.ts tests/secret-provider-revision.test.ts tests/secret-reference-resources.test.ts tests/secret-session-view.test.ts",
|
|
49
49
|
"test:watch": "vitest"
|
|
50
50
|
}
|
|
51
51
|
}
|