@ouro.bot/cli 0.1.0-alpha.981 → 0.1.0-alpha.982
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/changelog.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.982",
|
|
6
|
+
"changes": [
|
|
7
|
+
"An agent's psyche and friend records can no longer be reached by a write through a relative path, a case-altered path or a symlink."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
4
10
|
{
|
|
5
11
|
"version": "0.1.0-alpha.981",
|
|
6
12
|
"changes": [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.982</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.OURO_CLI_TRUST_MANIFEST = void 0;
|
|
37
|
+
exports.canonicalTarget = canonicalTarget;
|
|
38
|
+
exports.insideAgentFolder = insideAgentFolder;
|
|
37
39
|
exports.guardInvocation = guardInvocation;
|
|
38
40
|
const fs = __importStar(require("node:fs"));
|
|
39
41
|
const os = __importStar(require("node:os"));
|
|
@@ -42,6 +44,7 @@ const friends_1 = require("@ouro.bot/friends");
|
|
|
42
44
|
const shell_quote_1 = require("shell-quote");
|
|
43
45
|
const runtime_1 = require("../nerves/runtime");
|
|
44
46
|
const store_1 = require("../commerce/store");
|
|
47
|
+
const identity_1 = require("../heart/identity");
|
|
45
48
|
const deny = (reason) => ({ allowed: false, reason });
|
|
46
49
|
const allow = { allowed: true };
|
|
47
50
|
// --- reason templates ---
|
|
@@ -70,10 +73,68 @@ const PROTECTED_PATH_SEGMENTS = [
|
|
|
70
73
|
"state/reports/",
|
|
71
74
|
];
|
|
72
75
|
const PROTECTED_FILENAMES = ["agent.json", "tool-profiles.json"];
|
|
73
|
-
/**
|
|
76
|
+
/** How many symlinks a dangling-link walk follows before refusing (the Linux kernel's own limit is 40). */
|
|
77
|
+
const MAX_SYMLINK_HOPS = 40;
|
|
78
|
+
/** A lookup that failed only because part of the path does not exist yet; any other failure leaves the target unknown. */
|
|
79
|
+
function rethrowUnlessMissing(error) {
|
|
80
|
+
const code = error.code;
|
|
81
|
+
if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The real location of `lexical`: `realpathSync.native` of the deepest part that exists, the final file included, with the
|
|
86
|
+
* missing remainder appended. A dangling symlink is followed, from its real parent, to where a write through it would land.
|
|
87
|
+
* Throws when the filesystem cannot say where the path lands (a denied lookup, a symlink loop, too many links), so the
|
|
88
|
+
* guard can refuse rather than fall back to the lexical path.
|
|
89
|
+
*/
|
|
90
|
+
function canonicalize(lexical) {
|
|
91
|
+
let current = lexical;
|
|
92
|
+
const missing = [];
|
|
93
|
+
let hops = 0;
|
|
94
|
+
for (;;) {
|
|
95
|
+
try {
|
|
96
|
+
return path.join(fs.realpathSync.native(current), ...missing);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
rethrowUnlessMissing(error);
|
|
100
|
+
}
|
|
101
|
+
let stat;
|
|
102
|
+
try {
|
|
103
|
+
stat = fs.lstatSync(current);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
rethrowUnlessMissing(error);
|
|
107
|
+
}
|
|
108
|
+
if (stat?.isSymbolicLink()) {
|
|
109
|
+
if (hops >= MAX_SYMLINK_HOPS)
|
|
110
|
+
throw new Error(`too many symbolic links resolving ${lexical}`);
|
|
111
|
+
hops += 1;
|
|
112
|
+
current = path.resolve(fs.realpathSync.native(path.dirname(current)), fs.readlinkSync(current));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const parent = path.dirname(current);
|
|
116
|
+
if (parent === current)
|
|
117
|
+
return path.join(current, ...missing);
|
|
118
|
+
missing.unshift(path.basename(current));
|
|
119
|
+
current = parent;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Every place a file tool could write `p`: resolved against the agent root and against the repo root (which is what
|
|
124
|
+
* `write_file` and `edit_file` use), each in its lexical and canonical form. An absolute `p` is resolved only as itself.
|
|
125
|
+
*/
|
|
126
|
+
function canonicalTarget(p, agentRoot) {
|
|
127
|
+
const lexicals = path.isAbsolute(p) ? [p] : [path.resolve(agentRoot, p), path.resolve((0, identity_1.getRepoRoot)(), p)];
|
|
128
|
+
return lexicals.flatMap((lexical) => [lexical, canonicalize(lexical)]);
|
|
129
|
+
}
|
|
130
|
+
/** True when any form of `p` is the agent's `folder` (in either its lexical or its canonical form) or under it. */
|
|
131
|
+
function insideAgentFolder(p, agentRoot, folder) {
|
|
132
|
+
const lexicalFolder = path.resolve(agentRoot, folder);
|
|
133
|
+
const folders = [lexicalFolder, canonicalize(lexicalFolder)];
|
|
134
|
+
return canonicalTarget(p, agentRoot).some((target) => folders.some((dir) => target === dir || target.startsWith(`${dir}${path.sep}`)));
|
|
135
|
+
}
|
|
136
|
+
/** The agent's psyche folder ships only from the repository (a pull request and the replay gate), so no tool writes into it. The shell check uses this lexical form; the file tools use `insideAgentFolder`. */
|
|
74
137
|
function isPsychePath(filePath, agentRoot) {
|
|
75
|
-
if (!agentRoot || !filePath)
|
|
76
|
-
return false;
|
|
77
138
|
const psyche = path.resolve(agentRoot, "psyche");
|
|
78
139
|
const target = path.resolve(agentRoot, filePath);
|
|
79
140
|
return target === psyche || target.startsWith(`${psyche}${path.sep}`);
|
|
@@ -85,8 +146,6 @@ function isPsychePath(filePath, agentRoot) {
|
|
|
85
146
|
* only: a shell can still write anywhere its uid can, which is why delegated-command authority lives outside the bundle.
|
|
86
147
|
*/
|
|
87
148
|
function isFriendsStorePath(filePath, agentRoot) {
|
|
88
|
-
if (!agentRoot || !filePath)
|
|
89
|
-
return false;
|
|
90
149
|
const store = path.resolve(agentRoot, "friends");
|
|
91
150
|
const target = path.resolve(agentRoot, filePath);
|
|
92
151
|
return target === store || target.startsWith(`${store}${path.sep}`);
|
|
@@ -272,9 +331,21 @@ function checkDestructiveShellPatterns(toolName, args) {
|
|
|
272
331
|
function checkProtectedPaths(toolName, args, context) {
|
|
273
332
|
if (toolName === "write_file" || toolName === "edit_file") {
|
|
274
333
|
const filePath = args.path || "";
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
334
|
+
// The file tools resolve a relative path against the repo root and follow symlinks, so the guard checks every form.
|
|
335
|
+
// A path whose landing place the filesystem cannot report is refused rather than checked lexically.
|
|
336
|
+
const agentRoot = filePath ? context.agentRoot : undefined;
|
|
337
|
+
if (agentRoot) {
|
|
338
|
+
try {
|
|
339
|
+
if (insideAgentFolder(filePath, agentRoot, "psyche"))
|
|
340
|
+
return deny(REASONS.psychePath);
|
|
341
|
+
if (insideAgentFolder(filePath, agentRoot, "friends"))
|
|
342
|
+
return deny(REASONS.protectedPath);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return deny(REASONS.protectedPath);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (isProtectedPath(filePath))
|
|
278
349
|
return deny(REASONS.protectedPath);
|
|
279
350
|
}
|
|
280
351
|
if (toolName === "shell") {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.982",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.982",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|