@granular-software/sdk 0.4.36 → 0.4.38
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 +82 -2
- package/dist/agent-evals.d.mts +77 -3
- package/dist/agent-evals.d.ts +77 -3
- package/dist/agent-evals.js +2764 -643
- package/dist/agent-evals.js.map +1 -1
- package/dist/agent-evals.mjs +2763 -643
- package/dist/agent-evals.mjs.map +1 -1
- package/dist/agent-harness.d.mts +39 -4
- package/dist/agent-harness.d.ts +39 -4
- package/dist/agent-harness.js +1051 -456
- package/dist/agent-harness.js.map +1 -1
- package/dist/agent-harness.mjs +1049 -457
- package/dist/agent-harness.mjs.map +1 -1
- package/dist/cli/index.js +2470 -298
- package/dist/client-BNbWA9jQ.d.ts +1064 -0
- package/dist/client-HDJgcJC5.d.mts +1064 -0
- package/dist/index.d.mts +18 -5
- package/dist/index.d.ts +18 -5
- package/dist/index.js +2162 -575
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2152 -576
- package/dist/index.mjs.map +1 -1
- package/dist/spend-rzS1rlFr.d.mts +1559 -0
- package/dist/spend-rzS1rlFr.d.ts +1559 -0
- package/dist/spend.d.mts +2 -0
- package/dist/spend.d.ts +2 -0
- package/dist/spend.js +111 -0
- package/dist/spend.js.map +1 -0
- package/dist/spend.mjs +107 -0
- package/dist/spend.mjs.map +1 -0
- package/package.json +8 -1
- package/dist/client-Cq8onk2D.d.mts +0 -2402
- package/dist/client-Cq8onk2D.d.ts +0 -2402
package/dist/cli/index.js
CHANGED
|
@@ -1194,8 +1194,8 @@ var require_command = __commonJS({
|
|
|
1194
1194
|
"../../node_modules/commander/lib/command.js"(exports) {
|
|
1195
1195
|
var EventEmitter = __require("events").EventEmitter;
|
|
1196
1196
|
var childProcess = __require("child_process");
|
|
1197
|
-
var
|
|
1198
|
-
var
|
|
1197
|
+
var path7 = __require("path");
|
|
1198
|
+
var fs8 = __require("fs");
|
|
1199
1199
|
var process10 = __require("process");
|
|
1200
1200
|
var { Argument: Argument2, humanReadableArgName } = require_argument();
|
|
1201
1201
|
var { CommanderError: CommanderError2 } = require_error();
|
|
@@ -2175,7 +2175,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2175
2175
|
* @param {string} subcommandName
|
|
2176
2176
|
*/
|
|
2177
2177
|
_checkForMissingExecutable(executableFile, executableDir, subcommandName) {
|
|
2178
|
-
if (
|
|
2178
|
+
if (fs8.existsSync(executableFile)) return;
|
|
2179
2179
|
const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
|
|
2180
2180
|
const executableMissing = `'${executableFile}' does not exist
|
|
2181
2181
|
- if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
|
|
@@ -2193,11 +2193,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2193
2193
|
let launchWithNode = false;
|
|
2194
2194
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
2195
2195
|
function findFile(baseDir, baseName) {
|
|
2196
|
-
const localBin =
|
|
2197
|
-
if (
|
|
2198
|
-
if (sourceExt.includes(
|
|
2196
|
+
const localBin = path7.resolve(baseDir, baseName);
|
|
2197
|
+
if (fs8.existsSync(localBin)) return localBin;
|
|
2198
|
+
if (sourceExt.includes(path7.extname(baseName))) return void 0;
|
|
2199
2199
|
const foundExt = sourceExt.find(
|
|
2200
|
-
(ext) =>
|
|
2200
|
+
(ext) => fs8.existsSync(`${localBin}${ext}`)
|
|
2201
2201
|
);
|
|
2202
2202
|
if (foundExt) return `${localBin}${foundExt}`;
|
|
2203
2203
|
return void 0;
|
|
@@ -2209,21 +2209,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2209
2209
|
if (this._scriptPath) {
|
|
2210
2210
|
let resolvedScriptPath;
|
|
2211
2211
|
try {
|
|
2212
|
-
resolvedScriptPath =
|
|
2212
|
+
resolvedScriptPath = fs8.realpathSync(this._scriptPath);
|
|
2213
2213
|
} catch {
|
|
2214
2214
|
resolvedScriptPath = this._scriptPath;
|
|
2215
2215
|
}
|
|
2216
|
-
executableDir =
|
|
2217
|
-
|
|
2216
|
+
executableDir = path7.resolve(
|
|
2217
|
+
path7.dirname(resolvedScriptPath),
|
|
2218
2218
|
executableDir
|
|
2219
2219
|
);
|
|
2220
2220
|
}
|
|
2221
2221
|
if (executableDir) {
|
|
2222
2222
|
let localFile = findFile(executableDir, executableFile);
|
|
2223
2223
|
if (!localFile && !subcommand._executableFile && this._scriptPath) {
|
|
2224
|
-
const legacyName =
|
|
2224
|
+
const legacyName = path7.basename(
|
|
2225
2225
|
this._scriptPath,
|
|
2226
|
-
|
|
2226
|
+
path7.extname(this._scriptPath)
|
|
2227
2227
|
);
|
|
2228
2228
|
if (legacyName !== this._name) {
|
|
2229
2229
|
localFile = findFile(
|
|
@@ -2234,7 +2234,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2234
2234
|
}
|
|
2235
2235
|
executableFile = localFile || executableFile;
|
|
2236
2236
|
}
|
|
2237
|
-
launchWithNode = sourceExt.includes(
|
|
2237
|
+
launchWithNode = sourceExt.includes(path7.extname(executableFile));
|
|
2238
2238
|
let proc;
|
|
2239
2239
|
if (process10.platform !== "win32") {
|
|
2240
2240
|
if (launchWithNode) {
|
|
@@ -3081,7 +3081,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3081
3081
|
* @return {Command}
|
|
3082
3082
|
*/
|
|
3083
3083
|
nameFromFilename(filename) {
|
|
3084
|
-
this._name =
|
|
3084
|
+
this._name = path7.basename(filename, path7.extname(filename));
|
|
3085
3085
|
return this;
|
|
3086
3086
|
}
|
|
3087
3087
|
/**
|
|
@@ -3095,9 +3095,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3095
3095
|
* @param {string} [path]
|
|
3096
3096
|
* @return {(string|null|Command)}
|
|
3097
3097
|
*/
|
|
3098
|
-
executableDir(
|
|
3099
|
-
if (
|
|
3100
|
-
this._executableDir =
|
|
3098
|
+
executableDir(path8) {
|
|
3099
|
+
if (path8 === void 0) return this._executableDir;
|
|
3100
|
+
this._executableDir = path8;
|
|
3101
3101
|
return this;
|
|
3102
3102
|
}
|
|
3103
3103
|
/**
|
|
@@ -3440,8 +3440,8 @@ var require_package = __commonJS({
|
|
|
3440
3440
|
// ../../node_modules/dotenv/lib/main.js
|
|
3441
3441
|
var require_main = __commonJS({
|
|
3442
3442
|
"../../node_modules/dotenv/lib/main.js"(exports, module) {
|
|
3443
|
-
var
|
|
3444
|
-
var
|
|
3443
|
+
var fs8 = __require("fs");
|
|
3444
|
+
var path7 = __require("path");
|
|
3445
3445
|
var os2 = __require("os");
|
|
3446
3446
|
var crypto2 = __require("crypto");
|
|
3447
3447
|
var packageJson = require_package();
|
|
@@ -3549,7 +3549,7 @@ var require_main = __commonJS({
|
|
|
3549
3549
|
if (options && options.path && options.path.length > 0) {
|
|
3550
3550
|
if (Array.isArray(options.path)) {
|
|
3551
3551
|
for (const filepath of options.path) {
|
|
3552
|
-
if (
|
|
3552
|
+
if (fs8.existsSync(filepath)) {
|
|
3553
3553
|
possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
|
|
3554
3554
|
}
|
|
3555
3555
|
}
|
|
@@ -3557,15 +3557,15 @@ var require_main = __commonJS({
|
|
|
3557
3557
|
possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`;
|
|
3558
3558
|
}
|
|
3559
3559
|
} else {
|
|
3560
|
-
possibleVaultPath =
|
|
3560
|
+
possibleVaultPath = path7.resolve(process.cwd(), ".env.vault");
|
|
3561
3561
|
}
|
|
3562
|
-
if (
|
|
3562
|
+
if (fs8.existsSync(possibleVaultPath)) {
|
|
3563
3563
|
return possibleVaultPath;
|
|
3564
3564
|
}
|
|
3565
3565
|
return null;
|
|
3566
3566
|
}
|
|
3567
3567
|
function _resolveHome(envPath) {
|
|
3568
|
-
return envPath[0] === "~" ?
|
|
3568
|
+
return envPath[0] === "~" ? path7.join(os2.homedir(), envPath.slice(1)) : envPath;
|
|
3569
3569
|
}
|
|
3570
3570
|
function _configVault(options) {
|
|
3571
3571
|
const debug = Boolean(options && options.debug);
|
|
@@ -3582,7 +3582,7 @@ var require_main = __commonJS({
|
|
|
3582
3582
|
return { parsed };
|
|
3583
3583
|
}
|
|
3584
3584
|
function configDotenv(options) {
|
|
3585
|
-
const dotenvPath =
|
|
3585
|
+
const dotenvPath = path7.resolve(process.cwd(), ".env");
|
|
3586
3586
|
let encoding = "utf8";
|
|
3587
3587
|
const debug = Boolean(options && options.debug);
|
|
3588
3588
|
const quiet = options && "quiet" in options ? options.quiet : true;
|
|
@@ -3606,13 +3606,13 @@ var require_main = __commonJS({
|
|
|
3606
3606
|
}
|
|
3607
3607
|
let lastError;
|
|
3608
3608
|
const parsedAll = {};
|
|
3609
|
-
for (const
|
|
3609
|
+
for (const path8 of optionPaths) {
|
|
3610
3610
|
try {
|
|
3611
|
-
const parsed = DotenvModule.parse(
|
|
3611
|
+
const parsed = DotenvModule.parse(fs8.readFileSync(path8, { encoding }));
|
|
3612
3612
|
DotenvModule.populate(parsedAll, parsed, options);
|
|
3613
3613
|
} catch (e) {
|
|
3614
3614
|
if (debug) {
|
|
3615
|
-
_debug(`Failed to load ${
|
|
3615
|
+
_debug(`Failed to load ${path8} ${e.message}`);
|
|
3616
3616
|
}
|
|
3617
3617
|
lastError = e;
|
|
3618
3618
|
}
|
|
@@ -3627,8 +3627,8 @@ var require_main = __commonJS({
|
|
|
3627
3627
|
const shortPaths = [];
|
|
3628
3628
|
for (const filePath of optionPaths) {
|
|
3629
3629
|
try {
|
|
3630
|
-
const
|
|
3631
|
-
shortPaths.push(
|
|
3630
|
+
const relative2 = path7.relative(process.cwd(), filePath);
|
|
3631
|
+
shortPaths.push(relative2);
|
|
3632
3632
|
} catch (e) {
|
|
3633
3633
|
if (debug) {
|
|
3634
3634
|
_debug(`Failed to load ${filePath} ${e.message}`);
|
|
@@ -5561,7 +5561,7 @@ var LIBRARY_TEMPLATE = {
|
|
|
5561
5561
|
userId: "reader_demo",
|
|
5562
5562
|
name: "Mina Reader",
|
|
5563
5563
|
email: "mina@example.com",
|
|
5564
|
-
permissions: ["
|
|
5564
|
+
permissions: ["allow-all"]
|
|
5565
5565
|
},
|
|
5566
5566
|
seedRecords: [
|
|
5567
5567
|
{
|
|
@@ -5937,7 +5937,7 @@ var SUPPORT_TEMPLATE = {
|
|
|
5937
5937
|
userId: "support_demo",
|
|
5938
5938
|
name: "Taylor Support",
|
|
5939
5939
|
email: "taylor@example.com",
|
|
5940
|
-
permissions: ["
|
|
5940
|
+
permissions: ["allow-all"]
|
|
5941
5941
|
},
|
|
5942
5942
|
seedRecords: [
|
|
5943
5943
|
{
|
|
@@ -6253,7 +6253,7 @@ var DELIVERY_TEMPLATE = {
|
|
|
6253
6253
|
userId: "delivery_demo",
|
|
6254
6254
|
name: "Alex Delivery",
|
|
6255
6255
|
email: "alex@example.com",
|
|
6256
|
-
permissions: ["
|
|
6256
|
+
permissions: ["allow-all"]
|
|
6257
6257
|
},
|
|
6258
6258
|
seedRecords: [
|
|
6259
6259
|
{
|
|
@@ -6769,10 +6769,839 @@ return {
|
|
|
6769
6769
|
`;
|
|
6770
6770
|
}
|
|
6771
6771
|
|
|
6772
|
+
// ../policy-engine/src/index.ts
|
|
6773
|
+
var DECISIONS = /* @__PURE__ */ new Set(["allow", "confirm", "deny"]);
|
|
6774
|
+
var BUILTIN_PROFILE_NAMES = ["allow-all", "confirm-all", "deny-all"];
|
|
6775
|
+
function getBuiltinPermissionProfiles() {
|
|
6776
|
+
return [
|
|
6777
|
+
{
|
|
6778
|
+
schemaVersion: 1,
|
|
6779
|
+
name: "allow-all",
|
|
6780
|
+
label: "Allow all",
|
|
6781
|
+
description: "Every declared action is visible unless a manifest policy denies it.",
|
|
6782
|
+
source: "builtin",
|
|
6783
|
+
defaults: { actionPolicy: "allow" },
|
|
6784
|
+
policies: [],
|
|
6785
|
+
actions: []
|
|
6786
|
+
},
|
|
6787
|
+
{
|
|
6788
|
+
schemaVersion: 1,
|
|
6789
|
+
name: "confirm-all",
|
|
6790
|
+
label: "Confirm all",
|
|
6791
|
+
description: "Every declared action requires user confirmation before execution.",
|
|
6792
|
+
source: "builtin",
|
|
6793
|
+
defaults: { actionPolicy: "confirm" },
|
|
6794
|
+
policies: [],
|
|
6795
|
+
actions: []
|
|
6796
|
+
},
|
|
6797
|
+
{
|
|
6798
|
+
schemaVersion: 1,
|
|
6799
|
+
name: "deny-all",
|
|
6800
|
+
label: "Deny all",
|
|
6801
|
+
description: "No declared action is invocable.",
|
|
6802
|
+
source: "builtin",
|
|
6803
|
+
defaults: { actionPolicy: "deny" },
|
|
6804
|
+
policies: [],
|
|
6805
|
+
actions: []
|
|
6806
|
+
}
|
|
6807
|
+
];
|
|
6808
|
+
}
|
|
6809
|
+
function stableStringify(value) {
|
|
6810
|
+
return JSON.stringify(sortJson(value));
|
|
6811
|
+
}
|
|
6812
|
+
async function digestJson(value) {
|
|
6813
|
+
const payload = stableStringify(value);
|
|
6814
|
+
const cryptoLike = globalThis.crypto;
|
|
6815
|
+
if (cryptoLike?.subtle) {
|
|
6816
|
+
const data = new TextEncoder().encode(payload);
|
|
6817
|
+
const hash2 = await cryptoLike.subtle.digest("SHA-256", data);
|
|
6818
|
+
const hex = Array.from(new Uint8Array(hash2)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
6819
|
+
return `sha256:${hex}`;
|
|
6820
|
+
}
|
|
6821
|
+
let hash = 2166136261;
|
|
6822
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
6823
|
+
hash ^= payload.charCodeAt(index);
|
|
6824
|
+
hash = Math.imul(hash, 16777619);
|
|
6825
|
+
}
|
|
6826
|
+
return `fnv1a:${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
6827
|
+
}
|
|
6828
|
+
function sortJson(value) {
|
|
6829
|
+
if (Array.isArray(value)) return value.map(sortJson);
|
|
6830
|
+
if (isRecord(value)) {
|
|
6831
|
+
return Object.fromEntries(
|
|
6832
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, child]) => [key, sortJson(child)])
|
|
6833
|
+
);
|
|
6834
|
+
}
|
|
6835
|
+
return value;
|
|
6836
|
+
}
|
|
6837
|
+
function isRecord(value) {
|
|
6838
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
6839
|
+
}
|
|
6840
|
+
function normalizeProfileName(name) {
|
|
6841
|
+
if (typeof name !== "string") return null;
|
|
6842
|
+
const trimmed = name.trim();
|
|
6843
|
+
if (!/^[a-z0-9][a-z0-9._-]{1,63}$/.test(trimmed)) return null;
|
|
6844
|
+
return trimmed;
|
|
6845
|
+
}
|
|
6846
|
+
function normalizeDecision(value) {
|
|
6847
|
+
return DECISIONS.has(value) ? value : null;
|
|
6848
|
+
}
|
|
6849
|
+
function normalizePath(value) {
|
|
6850
|
+
if (Array.isArray(value)) {
|
|
6851
|
+
return value.map((part) => String(part)).filter(Boolean);
|
|
6852
|
+
}
|
|
6853
|
+
if (typeof value === "string") {
|
|
6854
|
+
return value.includes(".") ? value.split(".").filter(Boolean) : [value];
|
|
6855
|
+
}
|
|
6856
|
+
return [];
|
|
6857
|
+
}
|
|
6858
|
+
function firstDefinedValue(spec) {
|
|
6859
|
+
if ("value" in spec) return spec.value;
|
|
6860
|
+
if ("stringValue" in spec) return spec.stringValue;
|
|
6861
|
+
if ("numberValue" in spec) return spec.numberValue;
|
|
6862
|
+
if ("booleanValue" in spec) return spec.booleanValue;
|
|
6863
|
+
if ("state" in spec) return spec.state;
|
|
6864
|
+
return void 0;
|
|
6865
|
+
}
|
|
6866
|
+
function normalizeCondition(input) {
|
|
6867
|
+
if (input === void 0 || input === null) return { kind: "always" };
|
|
6868
|
+
if (!isRecord(input)) {
|
|
6869
|
+
throw new Error("Policy condition must be an object");
|
|
6870
|
+
}
|
|
6871
|
+
if (Array.isArray(input.all)) {
|
|
6872
|
+
return {
|
|
6873
|
+
kind: "all",
|
|
6874
|
+
conditions: input.all.map((item) => normalizeCondition(item))
|
|
6875
|
+
};
|
|
6876
|
+
}
|
|
6877
|
+
if (Array.isArray(input.any)) {
|
|
6878
|
+
return {
|
|
6879
|
+
kind: "any",
|
|
6880
|
+
conditions: input.any.map((item) => normalizeCondition(item))
|
|
6881
|
+
};
|
|
6882
|
+
}
|
|
6883
|
+
if (input.not !== void 0) {
|
|
6884
|
+
return { kind: "not", condition: normalizeCondition(input.not) };
|
|
6885
|
+
}
|
|
6886
|
+
for (const source of ["input", "object", "stateMachine"]) {
|
|
6887
|
+
const raw = input[source];
|
|
6888
|
+
if (!isRecord(raw)) continue;
|
|
6889
|
+
const operator = raw.operator;
|
|
6890
|
+
if (operator !== "eq" && operator !== "neq" && operator !== "gt" && operator !== "gte" && operator !== "lt" && operator !== "lte" && operator !== "contains" && operator !== "not_contains" && operator !== "starts_with" && operator !== "ends_with" && operator !== "exists") {
|
|
6891
|
+
throw new Error(`Unsupported policy operator: ${String(operator)}`);
|
|
6892
|
+
}
|
|
6893
|
+
if (source === "stateMachine") {
|
|
6894
|
+
const machine = typeof raw.machine === "string" ? raw.machine : "";
|
|
6895
|
+
if (!machine) throw new Error("stateMachine condition requires machine");
|
|
6896
|
+
return {
|
|
6897
|
+
kind: "predicate",
|
|
6898
|
+
source,
|
|
6899
|
+
path: [machine],
|
|
6900
|
+
machine,
|
|
6901
|
+
operator,
|
|
6902
|
+
value: firstDefinedValue(raw)
|
|
6903
|
+
};
|
|
6904
|
+
}
|
|
6905
|
+
const path7 = normalizePath(raw.path ?? raw.field ?? raw.input);
|
|
6906
|
+
if (path7.length === 0) {
|
|
6907
|
+
throw new Error(`${source} condition requires a path`);
|
|
6908
|
+
}
|
|
6909
|
+
return {
|
|
6910
|
+
kind: "predicate",
|
|
6911
|
+
source,
|
|
6912
|
+
path: path7,
|
|
6913
|
+
operator,
|
|
6914
|
+
value: firstDefinedValue(raw)
|
|
6915
|
+
};
|
|
6916
|
+
}
|
|
6917
|
+
throw new Error(
|
|
6918
|
+
"Policy condition must contain all, any, not, input, object, or stateMachine"
|
|
6919
|
+
);
|
|
6920
|
+
}
|
|
6921
|
+
function readPath(root, path7) {
|
|
6922
|
+
let current = root;
|
|
6923
|
+
for (const part of path7) {
|
|
6924
|
+
if (!isRecord(current) && !Array.isArray(current)) {
|
|
6925
|
+
return { exists: false, value: void 0 };
|
|
6926
|
+
}
|
|
6927
|
+
if (!(part in current)) {
|
|
6928
|
+
return { exists: false, value: void 0 };
|
|
6929
|
+
}
|
|
6930
|
+
current = current[part];
|
|
6931
|
+
}
|
|
6932
|
+
return { exists: true, value: current };
|
|
6933
|
+
}
|
|
6934
|
+
function evaluateCondition(condition, context) {
|
|
6935
|
+
switch (condition.kind) {
|
|
6936
|
+
case "always":
|
|
6937
|
+
return true;
|
|
6938
|
+
case "all":
|
|
6939
|
+
return condition.conditions.every(
|
|
6940
|
+
(child) => evaluateCondition(child, context)
|
|
6941
|
+
);
|
|
6942
|
+
case "any":
|
|
6943
|
+
return condition.conditions.some(
|
|
6944
|
+
(child) => evaluateCondition(child, context)
|
|
6945
|
+
);
|
|
6946
|
+
case "not":
|
|
6947
|
+
return !evaluateCondition(condition.condition, context);
|
|
6948
|
+
case "predicate":
|
|
6949
|
+
return evaluatePredicate(condition, context);
|
|
6950
|
+
}
|
|
6951
|
+
}
|
|
6952
|
+
function evaluatePredicate(predicate, context) {
|
|
6953
|
+
const target = predicate.source === "input" ? context.input : predicate.source === "object" ? context.object : context.stateMachines;
|
|
6954
|
+
const { exists, value } = readPath(target || {}, predicate.path);
|
|
6955
|
+
if (predicate.operator === "exists") return exists;
|
|
6956
|
+
if (!exists) return false;
|
|
6957
|
+
const expected = predicate.value;
|
|
6958
|
+
switch (predicate.operator) {
|
|
6959
|
+
case "eq":
|
|
6960
|
+
return value === expected;
|
|
6961
|
+
case "neq":
|
|
6962
|
+
return value !== expected;
|
|
6963
|
+
case "gt":
|
|
6964
|
+
return typeof value === "number" && typeof expected === "number" && value > expected;
|
|
6965
|
+
case "gte":
|
|
6966
|
+
return typeof value === "number" && typeof expected === "number" && value >= expected;
|
|
6967
|
+
case "lt":
|
|
6968
|
+
return typeof value === "number" && typeof expected === "number" && value < expected;
|
|
6969
|
+
case "lte":
|
|
6970
|
+
return typeof value === "number" && typeof expected === "number" && value <= expected;
|
|
6971
|
+
case "contains":
|
|
6972
|
+
return Array.isArray(value) ? value.includes(expected) : typeof value === "string" && typeof expected === "string" && value.includes(expected);
|
|
6973
|
+
case "not_contains":
|
|
6974
|
+
return Array.isArray(value) ? !value.includes(expected) : typeof value === "string" && typeof expected === "string" && !value.includes(expected);
|
|
6975
|
+
case "starts_with":
|
|
6976
|
+
return typeof value === "string" && typeof expected === "string" && value.startsWith(expected);
|
|
6977
|
+
case "ends_with":
|
|
6978
|
+
return typeof value === "string" && typeof expected === "string" && value.endsWith(expected);
|
|
6979
|
+
}
|
|
6980
|
+
}
|
|
6981
|
+
function matchRule(rule, context) {
|
|
6982
|
+
if (!evaluateCondition(rule.condition, context)) return null;
|
|
6983
|
+
return {
|
|
6984
|
+
id: rule.id,
|
|
6985
|
+
outcome: rule.outcome,
|
|
6986
|
+
origin: rule.origin,
|
|
6987
|
+
reason: rule.reason,
|
|
6988
|
+
summary: rule.summary
|
|
6989
|
+
};
|
|
6990
|
+
}
|
|
6991
|
+
function evaluatePolicies(input) {
|
|
6992
|
+
const context = input.context || {};
|
|
6993
|
+
const manifestRules = input.manifestRules || [];
|
|
6994
|
+
const profileRules = input.profileRules || [];
|
|
6995
|
+
const defaultActionPolicy = input.defaultActionPolicy || "deny";
|
|
6996
|
+
const buckets = [
|
|
6997
|
+
{
|
|
6998
|
+
outcome: "deny",
|
|
6999
|
+
rules: manifestRules.filter((rule) => rule.outcome === "deny")
|
|
7000
|
+
},
|
|
7001
|
+
{
|
|
7002
|
+
outcome: "deny",
|
|
7003
|
+
rules: profileRules.filter((rule) => rule.outcome === "deny")
|
|
7004
|
+
},
|
|
7005
|
+
{
|
|
7006
|
+
outcome: "confirm",
|
|
7007
|
+
rules: manifestRules.filter((rule) => rule.outcome === "confirm")
|
|
7008
|
+
},
|
|
7009
|
+
{
|
|
7010
|
+
outcome: "confirm",
|
|
7011
|
+
rules: profileRules.filter((rule) => rule.outcome === "confirm")
|
|
7012
|
+
},
|
|
7013
|
+
{
|
|
7014
|
+
outcome: "allow",
|
|
7015
|
+
rules: profileRules.filter((rule) => rule.outcome === "allow")
|
|
7016
|
+
}
|
|
7017
|
+
];
|
|
7018
|
+
for (const bucket of buckets) {
|
|
7019
|
+
const matches = bucket.rules.map((rule) => matchRule(rule, context)).filter((match) => match !== null);
|
|
7020
|
+
if (matches.length > 0) return { outcome: bucket.outcome, matches };
|
|
7021
|
+
}
|
|
7022
|
+
return {
|
|
7023
|
+
outcome: defaultActionPolicy,
|
|
7024
|
+
matches: defaultActionPolicy === "deny" ? [] : [
|
|
7025
|
+
{
|
|
7026
|
+
id: `profile-default-${defaultActionPolicy}`,
|
|
7027
|
+
outcome: defaultActionPolicy,
|
|
7028
|
+
origin: {
|
|
7029
|
+
kind: "permissionProfile",
|
|
7030
|
+
profileName: "active",
|
|
7031
|
+
source: "permissionProfile"
|
|
7032
|
+
},
|
|
7033
|
+
summary: `Profile default policy is ${defaultActionPolicy}`
|
|
7034
|
+
}
|
|
7035
|
+
]
|
|
7036
|
+
};
|
|
7037
|
+
}
|
|
7038
|
+
function ruleSummary(outcome, reason, condition) {
|
|
7039
|
+
if (reason) return reason;
|
|
7040
|
+
if (condition.kind === "always") return `${capitalize(outcome)} always`;
|
|
7041
|
+
return `${capitalize(outcome)} when ${summarizeCondition(condition)}`;
|
|
7042
|
+
}
|
|
7043
|
+
function capitalize(value) {
|
|
7044
|
+
return value.slice(0, 1).toUpperCase() + value.slice(1);
|
|
7045
|
+
}
|
|
7046
|
+
function summarizeCondition(condition) {
|
|
7047
|
+
switch (condition.kind) {
|
|
7048
|
+
case "always":
|
|
7049
|
+
return "always";
|
|
7050
|
+
case "all":
|
|
7051
|
+
return condition.conditions.map(summarizeCondition).join(" and ");
|
|
7052
|
+
case "any":
|
|
7053
|
+
return condition.conditions.map(summarizeCondition).join(" or ");
|
|
7054
|
+
case "not":
|
|
7055
|
+
return `not (${summarizeCondition(condition.condition)})`;
|
|
7056
|
+
case "predicate": {
|
|
7057
|
+
const path7 = condition.source === "stateMachine" ? `stateMachine.${condition.machine || condition.path.join(".")}` : `${condition.source}.${condition.path.join(".")}`;
|
|
7058
|
+
if (condition.operator === "exists") return `${path7} exists`;
|
|
7059
|
+
return `${path7} ${condition.operator} ${String(condition.value)}`;
|
|
7060
|
+
}
|
|
7061
|
+
}
|
|
7062
|
+
}
|
|
7063
|
+
function makeRule(input) {
|
|
7064
|
+
const condition = input.condition || { kind: "always" };
|
|
7065
|
+
return {
|
|
7066
|
+
...input,
|
|
7067
|
+
condition,
|
|
7068
|
+
summary: ruleSummary(input.outcome, input.reason, condition)
|
|
7069
|
+
};
|
|
7070
|
+
}
|
|
7071
|
+
function actionIdentity(action, on) {
|
|
7072
|
+
return on ? `${on}:${action}` : `global:${action}`;
|
|
7073
|
+
}
|
|
7074
|
+
function normalizeAttachedClass(value) {
|
|
7075
|
+
if (!value) return null;
|
|
7076
|
+
if (!value.startsWith("@")) return value;
|
|
7077
|
+
const slashIndex = value.indexOf("/");
|
|
7078
|
+
return slashIndex === -1 ? value : value.slice(slashIndex + 1);
|
|
7079
|
+
}
|
|
7080
|
+
function resolveEffectReference(effects2, action, on) {
|
|
7081
|
+
const normalizedOn = normalizeAttachedClass(on);
|
|
7082
|
+
const matches = effects2.filter((effect) => {
|
|
7083
|
+
if (effect.name !== action) return false;
|
|
7084
|
+
const attachedClass = normalizeAttachedClass(effect.attachedClass);
|
|
7085
|
+
return normalizedOn ? attachedClass === normalizedOn : !attachedClass;
|
|
7086
|
+
});
|
|
7087
|
+
if (matches.length === 1) return matches[0];
|
|
7088
|
+
if (!normalizedOn) {
|
|
7089
|
+
const byName = effects2.filter((effect) => effect.name === action);
|
|
7090
|
+
if (byName.length > 1) {
|
|
7091
|
+
const classes = byName.map(
|
|
7092
|
+
(effect) => normalizeAttachedClass(effect.attachedClass) || "global"
|
|
7093
|
+
).join(", ");
|
|
7094
|
+
throw new Error(
|
|
7095
|
+
`Action "${action}" exists on ${classes}. Add "on" to disambiguate.`
|
|
7096
|
+
);
|
|
7097
|
+
}
|
|
7098
|
+
}
|
|
7099
|
+
if (matches.length > 1) {
|
|
7100
|
+
throw new Error(`Action "${action}" on "${String(on)}" is ambiguous.`);
|
|
7101
|
+
}
|
|
7102
|
+
throw new Error(
|
|
7103
|
+
on ? `Action "${action}" on "${on}" was not found.` : `Global action "${action}" was not found.`
|
|
7104
|
+
);
|
|
7105
|
+
}
|
|
7106
|
+
function manifestPolicyRulesForEffect(effect, manifestId) {
|
|
7107
|
+
const policies = isRecord(effect.metamodels?.policies) ? effect.metamodels?.policies : null;
|
|
7108
|
+
if (!policies) return [];
|
|
7109
|
+
const rules = [];
|
|
7110
|
+
const add2 = (outcome, specs, prefix) => {
|
|
7111
|
+
if (!Array.isArray(specs)) return;
|
|
7112
|
+
specs.forEach((spec, index) => {
|
|
7113
|
+
if (!isRecord(spec)) {
|
|
7114
|
+
throw new Error(
|
|
7115
|
+
`Manifest policy ${prefix}[${index}] for ${effect.effectKey} must be an object`
|
|
7116
|
+
);
|
|
7117
|
+
}
|
|
7118
|
+
const condition = normalizeCondition(spec.when);
|
|
7119
|
+
rules.push(
|
|
7120
|
+
makeRule({
|
|
7121
|
+
id: typeof spec.id === "string" ? spec.id : `manifest-${prefix}-${effect.effectKey}-${index + 1}`,
|
|
7122
|
+
origin: { kind: "manifest", manifestId, effectKey: effect.effectKey },
|
|
7123
|
+
outcome,
|
|
7124
|
+
reason: typeof spec.reason === "string" ? spec.reason : void 0,
|
|
7125
|
+
condition
|
|
7126
|
+
})
|
|
7127
|
+
);
|
|
7128
|
+
});
|
|
7129
|
+
};
|
|
7130
|
+
add2("deny", policies.denyWhen, "denyWhen");
|
|
7131
|
+
add2("confirm", policies.confirmWhen, "confirmWhen");
|
|
7132
|
+
add2("allow", policies.allowWhen, "allowWhen");
|
|
7133
|
+
return rules;
|
|
7134
|
+
}
|
|
7135
|
+
function inputSchemaHasField(effect, path7) {
|
|
7136
|
+
if (path7.length === 0) return false;
|
|
7137
|
+
const properties = isRecord(effect.inputSchema?.properties) ? effect.inputSchema?.properties : {};
|
|
7138
|
+
return path7[0] in properties;
|
|
7139
|
+
}
|
|
7140
|
+
function validateLimitBands(effect, action, warnings) {
|
|
7141
|
+
const limits = action.limits || [];
|
|
7142
|
+
const byInput = /* @__PURE__ */ new Map();
|
|
7143
|
+
limits.forEach((limit, index) => {
|
|
7144
|
+
const path7 = normalizePath(limit.input);
|
|
7145
|
+
if (path7.length === 0) {
|
|
7146
|
+
throw new Error(`Limit ${index + 1} on ${action.action} requires input`);
|
|
7147
|
+
}
|
|
7148
|
+
if (!inputSchemaHasField(effect, path7)) {
|
|
7149
|
+
throw new Error(
|
|
7150
|
+
`Limit ${index + 1} on ${action.action} references unsupported input field "${path7.join(".")}"`
|
|
7151
|
+
);
|
|
7152
|
+
}
|
|
7153
|
+
if (!normalizeDecision(limit.decision)) {
|
|
7154
|
+
throw new Error(
|
|
7155
|
+
`Limit ${index + 1} on ${action.action} has invalid decision`
|
|
7156
|
+
);
|
|
7157
|
+
}
|
|
7158
|
+
if (limit.upTo === void 0 === (limit.above === void 0)) {
|
|
7159
|
+
throw new Error(
|
|
7160
|
+
`Limit ${index + 1} on ${action.action} must define exactly one of upTo or above`
|
|
7161
|
+
);
|
|
7162
|
+
}
|
|
7163
|
+
if (limit.reason === void 0) {
|
|
7164
|
+
warnings.push(`Limit ${index + 1} on ${action.action} has no reason`);
|
|
7165
|
+
}
|
|
7166
|
+
const key = path7.join(".");
|
|
7167
|
+
byInput.set(key, [...byInput.get(key) || [], limit]);
|
|
7168
|
+
});
|
|
7169
|
+
for (const [input, bands] of byInput.entries()) {
|
|
7170
|
+
const thresholds = /* @__PURE__ */ new Set();
|
|
7171
|
+
let previousUpTo = -Infinity;
|
|
7172
|
+
for (const band of bands) {
|
|
7173
|
+
const value = band.upTo ?? band.above;
|
|
7174
|
+
const key = `${band.upTo === void 0 ? "above" : "upTo"}:${value}`;
|
|
7175
|
+
if (thresholds.has(key)) {
|
|
7176
|
+
throw new Error(
|
|
7177
|
+
`Duplicate limit threshold ${key} for input "${input}"`
|
|
7178
|
+
);
|
|
7179
|
+
}
|
|
7180
|
+
thresholds.add(key);
|
|
7181
|
+
if (band.upTo !== void 0) {
|
|
7182
|
+
if (band.upTo < previousUpTo) {
|
|
7183
|
+
throw new Error(`Unordered limit bands for input "${input}"`);
|
|
7184
|
+
}
|
|
7185
|
+
previousUpTo = band.upTo;
|
|
7186
|
+
}
|
|
7187
|
+
}
|
|
7188
|
+
}
|
|
7189
|
+
}
|
|
7190
|
+
function compileProfileActionRules(input) {
|
|
7191
|
+
const { profile, action, effect, warnings } = input;
|
|
7192
|
+
const origin = {
|
|
7193
|
+
kind: "permissionProfile",
|
|
7194
|
+
profileName: action.ownerProfileName || profile.name,
|
|
7195
|
+
source: profile.source || "permissionProfile"
|
|
7196
|
+
};
|
|
7197
|
+
const rules = [];
|
|
7198
|
+
const baseId = action.id || `${profile.name}-${effect.effectKey}`;
|
|
7199
|
+
if (action.allow === "always") {
|
|
7200
|
+
rules.push(
|
|
7201
|
+
makeRule({
|
|
7202
|
+
id: `${baseId}-allow`,
|
|
7203
|
+
origin,
|
|
7204
|
+
outcome: "allow",
|
|
7205
|
+
reason: action.reason
|
|
7206
|
+
})
|
|
7207
|
+
);
|
|
7208
|
+
}
|
|
7209
|
+
if (action.confirm === "always") {
|
|
7210
|
+
rules.push(
|
|
7211
|
+
makeRule({
|
|
7212
|
+
id: `${baseId}-confirm`,
|
|
7213
|
+
origin,
|
|
7214
|
+
outcome: "confirm",
|
|
7215
|
+
reason: action.reason
|
|
7216
|
+
})
|
|
7217
|
+
);
|
|
7218
|
+
}
|
|
7219
|
+
if (action.deny === "always") {
|
|
7220
|
+
rules.push(
|
|
7221
|
+
makeRule({
|
|
7222
|
+
id: `${baseId}-deny`,
|
|
7223
|
+
origin,
|
|
7224
|
+
outcome: "deny",
|
|
7225
|
+
reason: action.reason
|
|
7226
|
+
})
|
|
7227
|
+
);
|
|
7228
|
+
}
|
|
7229
|
+
const addConditional = (outcome, specs, suffix) => {
|
|
7230
|
+
(specs || []).forEach((spec, index) => {
|
|
7231
|
+
rules.push(
|
|
7232
|
+
makeRule({
|
|
7233
|
+
id: spec.id || `${baseId}-${suffix}-${index + 1}`,
|
|
7234
|
+
origin,
|
|
7235
|
+
outcome,
|
|
7236
|
+
reason: spec.reason,
|
|
7237
|
+
condition: normalizeCondition(spec.when)
|
|
7238
|
+
})
|
|
7239
|
+
);
|
|
7240
|
+
});
|
|
7241
|
+
};
|
|
7242
|
+
addConditional("allow", action.allowWhen, "allowWhen");
|
|
7243
|
+
addConditional("confirm", action.confirmWhen, "confirmWhen");
|
|
7244
|
+
addConditional("deny", action.denyWhen, "denyWhen");
|
|
7245
|
+
validateLimitBands(effect, action, warnings);
|
|
7246
|
+
const previousUpToByInput = /* @__PURE__ */ new Map();
|
|
7247
|
+
(action.limits || []).forEach((limit, index) => {
|
|
7248
|
+
const path7 = normalizePath(limit.input);
|
|
7249
|
+
const pathKey = path7.join(".");
|
|
7250
|
+
const lowerBound = previousUpToByInput.get(pathKey);
|
|
7251
|
+
const upperCondition = {
|
|
7252
|
+
kind: "predicate",
|
|
7253
|
+
source: "input",
|
|
7254
|
+
path: path7,
|
|
7255
|
+
operator: limit.upTo !== void 0 ? "lte" : "gt",
|
|
7256
|
+
value: limit.upTo ?? limit.above
|
|
7257
|
+
};
|
|
7258
|
+
const condition = lowerBound !== void 0 && limit.upTo !== void 0 ? {
|
|
7259
|
+
kind: "all",
|
|
7260
|
+
conditions: [
|
|
7261
|
+
{
|
|
7262
|
+
kind: "predicate",
|
|
7263
|
+
source: "input",
|
|
7264
|
+
path: path7,
|
|
7265
|
+
operator: "gt",
|
|
7266
|
+
value: lowerBound
|
|
7267
|
+
},
|
|
7268
|
+
upperCondition
|
|
7269
|
+
]
|
|
7270
|
+
} : upperCondition;
|
|
7271
|
+
rules.push(
|
|
7272
|
+
makeRule({
|
|
7273
|
+
id: limit.id || `${baseId}-limit-${index + 1}`,
|
|
7274
|
+
origin,
|
|
7275
|
+
outcome: limit.decision,
|
|
7276
|
+
reason: limit.reason,
|
|
7277
|
+
condition
|
|
7278
|
+
})
|
|
7279
|
+
);
|
|
7280
|
+
if (limit.upTo !== void 0) previousUpToByInput.set(pathKey, limit.upTo);
|
|
7281
|
+
});
|
|
7282
|
+
return rules;
|
|
7283
|
+
}
|
|
7284
|
+
function flattenProfiles(profiles, errors) {
|
|
7285
|
+
const byName = /* @__PURE__ */ new Map();
|
|
7286
|
+
for (const profile of profiles) {
|
|
7287
|
+
const name = normalizeProfileName(profile.name);
|
|
7288
|
+
if (!name) {
|
|
7289
|
+
errors.push(`Invalid permission profile name: ${String(profile.name)}`);
|
|
7290
|
+
continue;
|
|
7291
|
+
}
|
|
7292
|
+
if (byName.has(name)) {
|
|
7293
|
+
errors.push(`Duplicate permission profile name: ${name}`);
|
|
7294
|
+
continue;
|
|
7295
|
+
}
|
|
7296
|
+
byName.set(name, {
|
|
7297
|
+
...profile,
|
|
7298
|
+
name,
|
|
7299
|
+
defaultsOwnerName: profile.defaultsOwnerName || name,
|
|
7300
|
+
policies: (profile.policies || []).map((policy) => ({
|
|
7301
|
+
...policy,
|
|
7302
|
+
ownerProfileName: policy.ownerProfileName || name
|
|
7303
|
+
})),
|
|
7304
|
+
actions: (profile.actions || []).map((action) => ({
|
|
7305
|
+
...action,
|
|
7306
|
+
ownerProfileName: action.ownerProfileName || name
|
|
7307
|
+
}))
|
|
7308
|
+
});
|
|
7309
|
+
}
|
|
7310
|
+
const flattening = /* @__PURE__ */ new Set();
|
|
7311
|
+
const flattened = /* @__PURE__ */ new Map();
|
|
7312
|
+
const flatten = (name, depth) => {
|
|
7313
|
+
if (depth > 5) {
|
|
7314
|
+
errors.push(`Permission profile inheritance depth exceeds 5 at ${name}`);
|
|
7315
|
+
return null;
|
|
7316
|
+
}
|
|
7317
|
+
const existing = flattened.get(name);
|
|
7318
|
+
if (existing) return existing;
|
|
7319
|
+
const profile = byName.get(name);
|
|
7320
|
+
if (!profile) {
|
|
7321
|
+
errors.push(`Missing parent permission profile: ${name}`);
|
|
7322
|
+
return null;
|
|
7323
|
+
}
|
|
7324
|
+
if (flattening.has(name)) {
|
|
7325
|
+
errors.push(`Permission profile inheritance cycle detected at ${name}`);
|
|
7326
|
+
return null;
|
|
7327
|
+
}
|
|
7328
|
+
flattening.add(name);
|
|
7329
|
+
let result = profile;
|
|
7330
|
+
if (profile.extends) {
|
|
7331
|
+
if (profile.defaults !== void 0) {
|
|
7332
|
+
errors.push(
|
|
7333
|
+
`Permission profile "${name}" extends "${profile.extends}" and cannot define defaults`
|
|
7334
|
+
);
|
|
7335
|
+
}
|
|
7336
|
+
const parent = flatten(profile.extends, depth + 1);
|
|
7337
|
+
if (parent) {
|
|
7338
|
+
const actionMap = /* @__PURE__ */ new Map();
|
|
7339
|
+
for (const action of parent.actions || []) {
|
|
7340
|
+
actionMap.set(actionIdentity(action.action, action.on), action);
|
|
7341
|
+
}
|
|
7342
|
+
for (const action of profile.actions || []) {
|
|
7343
|
+
actionMap.set(actionIdentity(action.action, action.on), action);
|
|
7344
|
+
}
|
|
7345
|
+
result = {
|
|
7346
|
+
...profile,
|
|
7347
|
+
defaults: parent.defaults,
|
|
7348
|
+
defaultsOwnerName: parent.defaultsOwnerName || parent.name,
|
|
7349
|
+
policies: [...parent.policies || [], ...profile.policies || []],
|
|
7350
|
+
actions: [...actionMap.values()]
|
|
7351
|
+
};
|
|
7352
|
+
}
|
|
7353
|
+
}
|
|
7354
|
+
flattening.delete(name);
|
|
7355
|
+
flattened.set(name, result);
|
|
7356
|
+
return result;
|
|
7357
|
+
};
|
|
7358
|
+
for (const name of byName.keys()) flatten(name, 0);
|
|
7359
|
+
return [...flattened.values()];
|
|
7360
|
+
}
|
|
7361
|
+
function compileProfile(input) {
|
|
7362
|
+
const { profile, effects: effects2, manifestRules, warnings } = input;
|
|
7363
|
+
const defaultActionPolicy = normalizeDecision(profile.defaults?.actionPolicy) || "deny";
|
|
7364
|
+
const actionRules = {};
|
|
7365
|
+
const addRules = (effectKey, rules) => {
|
|
7366
|
+
if (rules.length === 0) return;
|
|
7367
|
+
actionRules[effectKey] = [...actionRules[effectKey] || [], ...rules];
|
|
7368
|
+
};
|
|
7369
|
+
for (const policy of profile.policies || []) {
|
|
7370
|
+
const outcome = normalizeDecision(policy.outcome || policy.decision);
|
|
7371
|
+
if (!outcome)
|
|
7372
|
+
throw new Error(
|
|
7373
|
+
`Profile "${profile.name}" has a policy with invalid decision`
|
|
7374
|
+
);
|
|
7375
|
+
const condition = normalizeCondition(policy.when);
|
|
7376
|
+
const targetEffects = policy.action ? [resolveEffectReference(effects2, policy.action, policy.on)] : effects2;
|
|
7377
|
+
for (const effect of targetEffects) {
|
|
7378
|
+
addRules(effect.effectKey, [
|
|
7379
|
+
makeRule({
|
|
7380
|
+
id: policy.id || `${profile.name}-${effect.effectKey}-profile-policy`,
|
|
7381
|
+
origin: {
|
|
7382
|
+
kind: "permissionProfile",
|
|
7383
|
+
profileName: policy.ownerProfileName || profile.name,
|
|
7384
|
+
source: profile.source || "permissionProfile"
|
|
7385
|
+
},
|
|
7386
|
+
outcome,
|
|
7387
|
+
reason: policy.reason,
|
|
7388
|
+
condition
|
|
7389
|
+
})
|
|
7390
|
+
]);
|
|
7391
|
+
}
|
|
7392
|
+
}
|
|
7393
|
+
for (const action of profile.actions || []) {
|
|
7394
|
+
const effect = resolveEffectReference(effects2, action.action, action.on);
|
|
7395
|
+
addRules(
|
|
7396
|
+
effect.effectKey,
|
|
7397
|
+
compileProfileActionRules({ profile, action, effect, warnings })
|
|
7398
|
+
);
|
|
7399
|
+
}
|
|
7400
|
+
const actionVisibility = effects2.filter((effect) => {
|
|
7401
|
+
const rules = actionRules[effect.effectKey] || [];
|
|
7402
|
+
const denyVisibilityRules = [
|
|
7403
|
+
...manifestRules[effect.effectKey] || [],
|
|
7404
|
+
...rules
|
|
7405
|
+
];
|
|
7406
|
+
if (denyVisibilityRules.some(
|
|
7407
|
+
(rule) => rule.outcome === "deny" && rule.condition.kind === "always"
|
|
7408
|
+
)) {
|
|
7409
|
+
return false;
|
|
7410
|
+
}
|
|
7411
|
+
if (defaultActionPolicy === "allow" || defaultActionPolicy === "confirm")
|
|
7412
|
+
return true;
|
|
7413
|
+
return denyVisibilityRules.some(
|
|
7414
|
+
(rule) => rule.outcome === "allow" || rule.outcome === "confirm"
|
|
7415
|
+
);
|
|
7416
|
+
}).map((effect) => effect.effectKey).sort();
|
|
7417
|
+
const summaries = Object.entries(actionRules).flatMap(
|
|
7418
|
+
([effectKey, rules]) => rules.map((rule) => `${effectKey}: ${rule.summary}`)
|
|
7419
|
+
).sort();
|
|
7420
|
+
return {
|
|
7421
|
+
name: profile.name,
|
|
7422
|
+
label: profile.label,
|
|
7423
|
+
description: profile.description,
|
|
7424
|
+
source: profile.source || "permissionProfile",
|
|
7425
|
+
sourcePath: profile.sourcePath,
|
|
7426
|
+
defaultActionPolicy,
|
|
7427
|
+
defaultActionPolicyOwner: profile.defaultsOwnerName || profile.name,
|
|
7428
|
+
digest: "",
|
|
7429
|
+
actionRules,
|
|
7430
|
+
actionVisibility,
|
|
7431
|
+
summaries,
|
|
7432
|
+
flattened: profile
|
|
7433
|
+
};
|
|
7434
|
+
}
|
|
7435
|
+
function normalizeSourceProfiles(sources) {
|
|
7436
|
+
const sourceProfiles = (sources || []).filter((profile) => isRecord(profile)).map((profile) => ({
|
|
7437
|
+
...profile,
|
|
7438
|
+
source: profile.source || "permissionProfile"
|
|
7439
|
+
}));
|
|
7440
|
+
const nonBuiltin = sourceProfiles.filter(
|
|
7441
|
+
(profile) => !BUILTIN_PROFILE_NAMES.includes(
|
|
7442
|
+
profile.name
|
|
7443
|
+
)
|
|
7444
|
+
);
|
|
7445
|
+
return [...getBuiltinPermissionProfiles(), ...nonBuiltin];
|
|
7446
|
+
}
|
|
7447
|
+
async function compilePolicyBundle(input) {
|
|
7448
|
+
const errors = [];
|
|
7449
|
+
const warnings = [];
|
|
7450
|
+
const effects2 = [...input.effects].sort(
|
|
7451
|
+
(left, right) => left.effectKey.localeCompare(right.effectKey)
|
|
7452
|
+
);
|
|
7453
|
+
const profiles = flattenProfiles(
|
|
7454
|
+
normalizeSourceProfiles(input.profileSources),
|
|
7455
|
+
errors
|
|
7456
|
+
);
|
|
7457
|
+
const manifestRules = {};
|
|
7458
|
+
for (const effect of effects2) {
|
|
7459
|
+
try {
|
|
7460
|
+
const rules = manifestPolicyRulesForEffect(effect, input.manifestId);
|
|
7461
|
+
if (rules.length > 0) manifestRules[effect.effectKey] = rules;
|
|
7462
|
+
} catch (error2) {
|
|
7463
|
+
errors.push(String(error2 instanceof Error ? error2.message : error2));
|
|
7464
|
+
}
|
|
7465
|
+
}
|
|
7466
|
+
const compiledProfiles = {};
|
|
7467
|
+
for (const profile of profiles) {
|
|
7468
|
+
try {
|
|
7469
|
+
const defaultActionPolicy = profile.defaults?.actionPolicy;
|
|
7470
|
+
if (defaultActionPolicy !== void 0 && !normalizeDecision(defaultActionPolicy)) {
|
|
7471
|
+
throw new Error(
|
|
7472
|
+
`Permission profile "${profile.name}" has invalid defaults.actionPolicy`
|
|
7473
|
+
);
|
|
7474
|
+
}
|
|
7475
|
+
const compiled = compileProfile({
|
|
7476
|
+
profile,
|
|
7477
|
+
effects: effects2,
|
|
7478
|
+
manifestRules,
|
|
7479
|
+
warnings
|
|
7480
|
+
});
|
|
7481
|
+
compiled.digest = await digestJson({
|
|
7482
|
+
profile: compiled.flattened,
|
|
7483
|
+
actionRules: compiled.actionRules,
|
|
7484
|
+
defaultActionPolicy: compiled.defaultActionPolicy
|
|
7485
|
+
});
|
|
7486
|
+
compiledProfiles[profile.name] = compiled;
|
|
7487
|
+
} catch (error2) {
|
|
7488
|
+
errors.push(String(error2 instanceof Error ? error2.message : error2));
|
|
7489
|
+
}
|
|
7490
|
+
}
|
|
7491
|
+
const permissionProfilesDigest = await digestJson(
|
|
7492
|
+
Object.values(compiledProfiles).map((profile) => ({
|
|
7493
|
+
name: profile.name,
|
|
7494
|
+
digest: profile.digest,
|
|
7495
|
+
source: profile.source
|
|
7496
|
+
}))
|
|
7497
|
+
);
|
|
7498
|
+
const bundleWithoutDigest = {
|
|
7499
|
+
schemaVersion: 1,
|
|
7500
|
+
buildId: input.buildId,
|
|
7501
|
+
sandboxId: input.sandboxId,
|
|
7502
|
+
manifestDigest: input.manifestDigest,
|
|
7503
|
+
permissionProfilesDigest,
|
|
7504
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7505
|
+
effects: effects2.map((effect) => ({
|
|
7506
|
+
effectKey: effect.effectKey,
|
|
7507
|
+
name: effect.name,
|
|
7508
|
+
attachedClass: normalizeAttachedClass(effect.attachedClass),
|
|
7509
|
+
isStatic: effect.isStatic === true
|
|
7510
|
+
})),
|
|
7511
|
+
manifestRules,
|
|
7512
|
+
profiles: compiledProfiles
|
|
7513
|
+
};
|
|
7514
|
+
const bundleDigest = await digestJson(bundleWithoutDigest);
|
|
7515
|
+
return {
|
|
7516
|
+
bundle: { ...bundleWithoutDigest, bundleDigest },
|
|
7517
|
+
errors,
|
|
7518
|
+
warnings
|
|
7519
|
+
};
|
|
7520
|
+
}
|
|
7521
|
+
function evaluateCompiledPolicyBundle(input) {
|
|
7522
|
+
const profile = input.bundle.profiles[input.profileName];
|
|
7523
|
+
if (!profile) {
|
|
7524
|
+
return {
|
|
7525
|
+
outcome: "deny",
|
|
7526
|
+
matches: [
|
|
7527
|
+
{
|
|
7528
|
+
id: "missing-profile",
|
|
7529
|
+
outcome: "deny",
|
|
7530
|
+
origin: {
|
|
7531
|
+
kind: "permissionProfile",
|
|
7532
|
+
profileName: input.profileName,
|
|
7533
|
+
source: "permissionProfile"
|
|
7534
|
+
},
|
|
7535
|
+
reason: `Permission profile "${input.profileName}" does not exist in this version`,
|
|
7536
|
+
summary: `Permission profile "${input.profileName}" does not exist in this version`
|
|
7537
|
+
}
|
|
7538
|
+
]
|
|
7539
|
+
};
|
|
7540
|
+
}
|
|
7541
|
+
if (!profile.actionVisibility.includes(input.effectKey)) {
|
|
7542
|
+
return {
|
|
7543
|
+
outcome: "deny",
|
|
7544
|
+
matches: [
|
|
7545
|
+
{
|
|
7546
|
+
id: "action-not-visible",
|
|
7547
|
+
outcome: "deny",
|
|
7548
|
+
origin: {
|
|
7549
|
+
kind: "permissionProfile",
|
|
7550
|
+
profileName: input.profileName,
|
|
7551
|
+
source: profile.source
|
|
7552
|
+
},
|
|
7553
|
+
reason: `Action ${input.effectKey} is not visible for ${input.profileName}`,
|
|
7554
|
+
summary: `Action ${input.effectKey} is not visible for ${input.profileName}`
|
|
7555
|
+
}
|
|
7556
|
+
]
|
|
7557
|
+
};
|
|
7558
|
+
}
|
|
7559
|
+
const decision = evaluatePolicies({
|
|
7560
|
+
manifestRules: input.bundle.manifestRules[input.effectKey] || [],
|
|
7561
|
+
profileRules: profile.actionRules[input.effectKey] || [],
|
|
7562
|
+
defaultActionPolicy: profile.defaultActionPolicy,
|
|
7563
|
+
context: input.context
|
|
7564
|
+
});
|
|
7565
|
+
return {
|
|
7566
|
+
outcome: decision.outcome,
|
|
7567
|
+
matches: decision.matches.map(
|
|
7568
|
+
(match) => match.id === `profile-default-${decision.outcome}` ? {
|
|
7569
|
+
...match,
|
|
7570
|
+
origin: {
|
|
7571
|
+
kind: "permissionProfile",
|
|
7572
|
+
profileName: profile.name,
|
|
7573
|
+
source: profile.source
|
|
7574
|
+
}
|
|
7575
|
+
} : match
|
|
7576
|
+
)
|
|
7577
|
+
};
|
|
7578
|
+
}
|
|
7579
|
+
function extractManifestEffects(manifest) {
|
|
7580
|
+
const effects2 = [];
|
|
7581
|
+
for (const volume of manifest.volumes || []) {
|
|
7582
|
+
for (const operation of volume.operations || []) {
|
|
7583
|
+
const effect = operation.withEffect;
|
|
7584
|
+
if (!effect?.name) continue;
|
|
7585
|
+
const attachedClass = normalizeAttachedClass(effect.attachedClass);
|
|
7586
|
+
const effectKey = attachedClass ? effect.isStatic ? `class:${attachedClass}:static:${effect.name}` : `class:${attachedClass}:instance:${effect.name}` : `global:${effect.name}`;
|
|
7587
|
+
effects2.push({
|
|
7588
|
+
effectKey,
|
|
7589
|
+
name: effect.name,
|
|
7590
|
+
attachedClass,
|
|
7591
|
+
isStatic: effect.isStatic === true,
|
|
7592
|
+
inputSchema: effect.inputSchema || null,
|
|
7593
|
+
metamodels: effect.metamodels || null
|
|
7594
|
+
});
|
|
7595
|
+
}
|
|
7596
|
+
}
|
|
7597
|
+
return effects2;
|
|
7598
|
+
}
|
|
7599
|
+
|
|
6772
7600
|
// src/cli/config.ts
|
|
6773
7601
|
var MANIFEST_FILE = "granular.json";
|
|
6774
7602
|
var RC_FILE = ".granularrc";
|
|
6775
7603
|
var ENV_LOCAL_FILE = ".env.local";
|
|
7604
|
+
var PERMISSIONS_DIR = "permissions";
|
|
6776
7605
|
function getProjectRoot() {
|
|
6777
7606
|
return process.cwd();
|
|
6778
7607
|
}
|
|
@@ -6785,6 +7614,9 @@ function getRcPath() {
|
|
|
6785
7614
|
function getEnvLocalPath() {
|
|
6786
7615
|
return path3__namespace.join(getProjectRoot(), ENV_LOCAL_FILE);
|
|
6787
7616
|
}
|
|
7617
|
+
function getPermissionsDir() {
|
|
7618
|
+
return path3__namespace.join(getProjectRoot(), PERMISSIONS_DIR);
|
|
7619
|
+
}
|
|
6788
7620
|
function readManifestFile() {
|
|
6789
7621
|
const filePath = getManifestPath();
|
|
6790
7622
|
if (!fs__namespace.existsSync(filePath)) return null;
|
|
@@ -6799,6 +7631,40 @@ function writeManifestFile(project) {
|
|
|
6799
7631
|
const filePath = getManifestPath();
|
|
6800
7632
|
fs__namespace.writeFileSync(filePath, JSON.stringify(project, null, 2) + "\n", "utf-8");
|
|
6801
7633
|
}
|
|
7634
|
+
function ensureBuiltinPermissionFiles() {
|
|
7635
|
+
const permissionsDir = getPermissionsDir();
|
|
7636
|
+
if (!fs__namespace.existsSync(permissionsDir)) {
|
|
7637
|
+
fs__namespace.mkdirSync(permissionsDir, { recursive: true });
|
|
7638
|
+
}
|
|
7639
|
+
for (const profile of getBuiltinPermissionProfiles()) {
|
|
7640
|
+
const filePath = path3__namespace.join(permissionsDir, `${profile.name}.json`);
|
|
7641
|
+
if (!fs__namespace.existsSync(filePath)) {
|
|
7642
|
+
fs__namespace.writeFileSync(filePath, JSON.stringify(profile, null, 2) + "\n", "utf-8");
|
|
7643
|
+
}
|
|
7644
|
+
}
|
|
7645
|
+
}
|
|
7646
|
+
function readPermissionProfileFiles() {
|
|
7647
|
+
const permissionsDir = getPermissionsDir();
|
|
7648
|
+
if (!fs__namespace.existsSync(permissionsDir)) {
|
|
7649
|
+
return getBuiltinPermissionProfiles();
|
|
7650
|
+
}
|
|
7651
|
+
const profiles = [];
|
|
7652
|
+
const entries = fs__namespace.readdirSync(permissionsDir).filter((entry) => entry.endsWith(".json")).sort();
|
|
7653
|
+
for (const entry of entries) {
|
|
7654
|
+
const filePath = path3__namespace.join(permissionsDir, entry);
|
|
7655
|
+
const raw = fs__namespace.readFileSync(filePath, "utf-8");
|
|
7656
|
+
const parsed = JSON.parse(raw);
|
|
7657
|
+
profiles.push({
|
|
7658
|
+
...parsed,
|
|
7659
|
+
name: parsed.name || entry.replace(/\.json$/, ""),
|
|
7660
|
+
sourcePath: path3__namespace.relative(getProjectRoot(), filePath)
|
|
7661
|
+
});
|
|
7662
|
+
}
|
|
7663
|
+
if (profiles.length === 0) {
|
|
7664
|
+
return getBuiltinPermissionProfiles();
|
|
7665
|
+
}
|
|
7666
|
+
return profiles;
|
|
7667
|
+
}
|
|
6802
7668
|
function manifestExists() {
|
|
6803
7669
|
return fs__namespace.existsSync(getManifestPath());
|
|
6804
7670
|
}
|
|
@@ -6926,8 +7792,8 @@ var ApiClient = class {
|
|
|
6926
7792
|
this.apiKey = resolveAuthTokenForApiUrl(apiKey, apiUrl);
|
|
6927
7793
|
this.baseUrl = apiUrl.replace("wss://", "https://").replace("ws://", "http://").replace(/\/ws$/, "");
|
|
6928
7794
|
}
|
|
6929
|
-
async request(
|
|
6930
|
-
const url = `${this.baseUrl}${
|
|
7795
|
+
async request(path7, options = {}) {
|
|
7796
|
+
const url = `${this.baseUrl}${path7}`;
|
|
6931
7797
|
const response = await fetch(url, {
|
|
6932
7798
|
...options,
|
|
6933
7799
|
headers: {
|
|
@@ -7098,31 +7964,52 @@ var ApiClient = class {
|
|
|
7098
7964
|
}
|
|
7099
7965
|
// ── Permission Profiles ──
|
|
7100
7966
|
async createPermissionProfile(sandboxId, data) {
|
|
7967
|
+
const profile = {
|
|
7968
|
+
...data.rules,
|
|
7969
|
+
schemaVersion: 1,
|
|
7970
|
+
name: data.name
|
|
7971
|
+
};
|
|
7972
|
+
const result = await this.syncPermissionProfileSources(sandboxId, [profile]);
|
|
7973
|
+
const synced = result.items?.find((item) => item.name === data.name) || result.items?.[0];
|
|
7974
|
+
if (!synced) {
|
|
7975
|
+
throw new Error(`Permission profile source sync did not return ${data.name}`);
|
|
7976
|
+
}
|
|
7977
|
+
return synced;
|
|
7978
|
+
}
|
|
7979
|
+
async ensureDefaultProfile(sandboxId) {
|
|
7980
|
+
return await this.createPermissionProfile(sandboxId, {
|
|
7981
|
+
name: "allow-all",
|
|
7982
|
+
rules: {
|
|
7983
|
+
schemaVersion: 1,
|
|
7984
|
+
name: "allow-all",
|
|
7985
|
+
description: "Every declared action is visible unless a manifest policy denies it.",
|
|
7986
|
+
defaults: { actionPolicy: "allow" },
|
|
7987
|
+
actions: []
|
|
7988
|
+
}
|
|
7989
|
+
});
|
|
7990
|
+
}
|
|
7991
|
+
async syncPermissionProfileSources(sandboxId, profiles) {
|
|
7101
7992
|
return this.request(
|
|
7102
|
-
`/control/sandboxes/${sandboxId}/permission-
|
|
7993
|
+
`/control/sandboxes/${sandboxId}/permission-profile-sources`,
|
|
7103
7994
|
{
|
|
7104
|
-
method: "
|
|
7105
|
-
body: JSON.stringify(
|
|
7995
|
+
method: "PUT",
|
|
7996
|
+
body: JSON.stringify({ profiles })
|
|
7106
7997
|
}
|
|
7107
7998
|
);
|
|
7108
7999
|
}
|
|
7109
|
-
async
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
}
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
const existing = profiles.items.find((p) => p.name === "default");
|
|
7123
|
-
if (existing) return existing;
|
|
7124
|
-
throw new Error("Could not create or find default permission profile");
|
|
7125
|
-
}
|
|
8000
|
+
async getVersionPolicies(versionId) {
|
|
8001
|
+
return this.request(`/control/ontology-versions/${versionId}/policies`);
|
|
8002
|
+
}
|
|
8003
|
+
async getVersionPermissionProfiles(versionId) {
|
|
8004
|
+
return this.request(
|
|
8005
|
+
`/control/ontology-versions/${versionId}/permission-profiles`
|
|
8006
|
+
);
|
|
8007
|
+
}
|
|
8008
|
+
async evaluateVersionPolicy(versionId, request) {
|
|
8009
|
+
return this.request(`/control/builds/${versionId}/policies/evaluate`, {
|
|
8010
|
+
method: "POST",
|
|
8011
|
+
body: JSON.stringify(request)
|
|
8012
|
+
});
|
|
7126
8013
|
}
|
|
7127
8014
|
// ── Subjects ──
|
|
7128
8015
|
async createSubject(identityId, name) {
|
|
@@ -9521,8 +10408,8 @@ function getErrorMap() {
|
|
|
9521
10408
|
|
|
9522
10409
|
// ../../node_modules/zod/v3/helpers/parseUtil.js
|
|
9523
10410
|
var makeIssue = (params) => {
|
|
9524
|
-
const { data, path:
|
|
9525
|
-
const fullPath = [...
|
|
10411
|
+
const { data, path: path7, errorMaps, issueData } = params;
|
|
10412
|
+
const fullPath = [...path7, ...issueData.path || []];
|
|
9526
10413
|
const fullIssue = {
|
|
9527
10414
|
...issueData,
|
|
9528
10415
|
path: fullPath
|
|
@@ -9638,11 +10525,11 @@ var errorUtil;
|
|
|
9638
10525
|
|
|
9639
10526
|
// ../../node_modules/zod/v3/types.js
|
|
9640
10527
|
var ParseInputLazyPath = class {
|
|
9641
|
-
constructor(parent, value,
|
|
10528
|
+
constructor(parent, value, path7, key) {
|
|
9642
10529
|
this._cachedPath = [];
|
|
9643
10530
|
this.parent = parent;
|
|
9644
10531
|
this.data = value;
|
|
9645
|
-
this._path =
|
|
10532
|
+
this._path = path7;
|
|
9646
10533
|
this._key = key;
|
|
9647
10534
|
}
|
|
9648
10535
|
get path() {
|
|
@@ -13141,12 +14028,73 @@ var StateMachineTransitionSchema = external_exports.object({
|
|
|
13141
14028
|
from: external_exports.string().min(1),
|
|
13142
14029
|
to: external_exports.string().min(1)
|
|
13143
14030
|
}).strict();
|
|
13144
|
-
external_exports.object({
|
|
13145
|
-
name: external_exports.string().min(1),
|
|
13146
|
-
entryState: external_exports.string().min(1),
|
|
13147
|
-
states: external_exports.array(StateMachineStateSchema).min(1),
|
|
13148
|
-
transitions: external_exports.array(StateMachineTransitionSchema),
|
|
13149
|
-
finalStates: external_exports.array(external_exports.string()).optional()
|
|
14031
|
+
external_exports.object({
|
|
14032
|
+
name: external_exports.string().min(1),
|
|
14033
|
+
entryState: external_exports.string().min(1),
|
|
14034
|
+
states: external_exports.array(StateMachineStateSchema).min(1),
|
|
14035
|
+
transitions: external_exports.array(StateMachineTransitionSchema),
|
|
14036
|
+
finalStates: external_exports.array(external_exports.string()).optional()
|
|
14037
|
+
}).strict();
|
|
14038
|
+
var POLICY_OPERATORS = [
|
|
14039
|
+
"eq",
|
|
14040
|
+
"neq",
|
|
14041
|
+
"gt",
|
|
14042
|
+
"gte",
|
|
14043
|
+
"lt",
|
|
14044
|
+
"lte",
|
|
14045
|
+
"contains",
|
|
14046
|
+
"not_contains",
|
|
14047
|
+
"starts_with",
|
|
14048
|
+
"ends_with",
|
|
14049
|
+
"exists"
|
|
14050
|
+
];
|
|
14051
|
+
var PolicyPredicateSchema = external_exports.object({
|
|
14052
|
+
path: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).optional(),
|
|
14053
|
+
field: external_exports.string().optional(),
|
|
14054
|
+
input: external_exports.string().optional(),
|
|
14055
|
+
operator: external_exports.enum([...POLICY_OPERATORS]),
|
|
14056
|
+
stringValue: external_exports.string().optional(),
|
|
14057
|
+
numberValue: external_exports.number().optional(),
|
|
14058
|
+
booleanValue: external_exports.boolean().optional(),
|
|
14059
|
+
value: external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean(), external_exports.null()]).optional()
|
|
14060
|
+
}).strict();
|
|
14061
|
+
var PolicyStateMachinePredicateSchema = external_exports.object({
|
|
14062
|
+
machine: external_exports.string().min(1),
|
|
14063
|
+
operator: external_exports.enum([...POLICY_OPERATORS]),
|
|
14064
|
+
state: external_exports.string().optional(),
|
|
14065
|
+
stringValue: external_exports.string().optional()
|
|
14066
|
+
}).strict();
|
|
14067
|
+
var PolicyConditionSchema = external_exports.lazy(
|
|
14068
|
+
() => external_exports.object({
|
|
14069
|
+
all: external_exports.array(PolicyConditionSchema).optional(),
|
|
14070
|
+
any: external_exports.array(PolicyConditionSchema).optional(),
|
|
14071
|
+
not: PolicyConditionSchema.optional(),
|
|
14072
|
+
input: PolicyPredicateSchema.optional(),
|
|
14073
|
+
object: PolicyPredicateSchema.optional(),
|
|
14074
|
+
stateMachine: PolicyStateMachinePredicateSchema.optional()
|
|
14075
|
+
}).strict().refine(
|
|
14076
|
+
(data) => [
|
|
14077
|
+
data.all,
|
|
14078
|
+
data.any,
|
|
14079
|
+
data.not,
|
|
14080
|
+
data.input,
|
|
14081
|
+
data.object,
|
|
14082
|
+
data.stateMachine
|
|
14083
|
+
].filter((value) => value !== void 0).length === 1,
|
|
14084
|
+
{
|
|
14085
|
+
message: "Policy condition must define exactly one of all, any, not, input, object, or stateMachine"
|
|
14086
|
+
}
|
|
14087
|
+
)
|
|
14088
|
+
);
|
|
14089
|
+
var PolicyRuleSchema = external_exports.object({
|
|
14090
|
+
id: external_exports.string().min(1).optional(),
|
|
14091
|
+
reason: external_exports.string().optional(),
|
|
14092
|
+
when: PolicyConditionSchema
|
|
14093
|
+
}).strict();
|
|
14094
|
+
var PoliciesSchema = external_exports.object({
|
|
14095
|
+
allowWhen: external_exports.array(PolicyRuleSchema).optional(),
|
|
14096
|
+
confirmWhen: external_exports.array(PolicyRuleSchema).optional(),
|
|
14097
|
+
denyWhen: external_exports.array(PolicyRuleSchema).optional()
|
|
13150
14098
|
}).strict();
|
|
13151
14099
|
external_exports.object({
|
|
13152
14100
|
postCondition: external_exports.union([
|
|
@@ -13177,7 +14125,8 @@ external_exports.object({
|
|
|
13177
14125
|
reason: external_exports.string().optional(),
|
|
13178
14126
|
mode: external_exports.string().optional()
|
|
13179
14127
|
}).strict()
|
|
13180
|
-
]).optional()
|
|
14128
|
+
]).optional(),
|
|
14129
|
+
policies: PoliciesSchema.optional()
|
|
13181
14130
|
}).strict();
|
|
13182
14131
|
|
|
13183
14132
|
// ../metamodel-core/src/index.ts
|
|
@@ -14046,6 +14995,54 @@ var noteMetamodelPackage = defineMetamodelPackage({
|
|
|
14046
14995
|
}
|
|
14047
14996
|
});
|
|
14048
14997
|
|
|
14998
|
+
// ../metamodel-policy/src/index.ts
|
|
14999
|
+
function escapeGraphqlString(value) {
|
|
15000
|
+
return JSON.stringify(value);
|
|
15001
|
+
}
|
|
15002
|
+
function buildPolicyMutations(effectKey, spec) {
|
|
15003
|
+
const policies = spec.policies;
|
|
15004
|
+
if (!policies) return [];
|
|
15005
|
+
const mutations = [];
|
|
15006
|
+
const addRules = (key, outcome) => {
|
|
15007
|
+
const rules = policies[key] || [];
|
|
15008
|
+
rules.forEach((rule, index) => {
|
|
15009
|
+
const condition = normalizeCondition(rule.when);
|
|
15010
|
+
const summary = rule.reason || summarizeCondition(condition);
|
|
15011
|
+
const id = rule.id || `${effectKey}:${outcome}:${index + 1}`;
|
|
15012
|
+
mutations.push({
|
|
15013
|
+
label: `set policy ${outcome} on ${effectKey}`,
|
|
15014
|
+
query: `mutation { set_policy_rule(effect_key: ${escapeGraphqlString(effectKey)}, policy_id: ${escapeGraphqlString(id)}, outcome: ${escapeGraphqlString(outcome)}, reason: ${escapeGraphqlString(summary)}, condition_json: ${escapeGraphqlString(JSON.stringify(condition))}) }`
|
|
15015
|
+
});
|
|
15016
|
+
});
|
|
15017
|
+
};
|
|
15018
|
+
addRules("allowWhen", "allow");
|
|
15019
|
+
addRules("confirmWhen", "confirm");
|
|
15020
|
+
addRules("denyWhen", "deny");
|
|
15021
|
+
return mutations;
|
|
15022
|
+
}
|
|
15023
|
+
var policyMetamodelPackage = defineMetamodelPackage({
|
|
15024
|
+
id: "policy",
|
|
15025
|
+
manifest: {
|
|
15026
|
+
buildEffectMutations: buildPolicyMutations
|
|
15027
|
+
},
|
|
15028
|
+
summary: {
|
|
15029
|
+
selections: {
|
|
15030
|
+
methodFields: ["policies"]
|
|
15031
|
+
},
|
|
15032
|
+
readMethodSummary(rawMethod) {
|
|
15033
|
+
return rawMethod.policies ? { metamodels: { policies: rawMethod.policies } } : {};
|
|
15034
|
+
}
|
|
15035
|
+
},
|
|
15036
|
+
docs: {
|
|
15037
|
+
effectRows: [
|
|
15038
|
+
{
|
|
15039
|
+
key: "policies",
|
|
15040
|
+
description: "Universal effect policies with allowWhen, confirmWhen, and denyWhen structural conditions."
|
|
15041
|
+
}
|
|
15042
|
+
]
|
|
15043
|
+
}
|
|
15044
|
+
});
|
|
15045
|
+
|
|
14049
15046
|
// ../metamodel-required/src/index.ts
|
|
14050
15047
|
function buildRequiredFieldMutations(fieldPath, required) {
|
|
14051
15048
|
if (!required) return [];
|
|
@@ -14820,7 +15817,8 @@ var DEFAULT_METAMODEL_PACKAGES = [
|
|
|
14820
15817
|
searchableMetamodelPackage,
|
|
14821
15818
|
validationRuleMetamodelPackage,
|
|
14822
15819
|
stateMachineMetamodelPackage,
|
|
14823
|
-
effectBehaviorsMetamodelPackage
|
|
15820
|
+
effectBehaviorsMetamodelPackage,
|
|
15821
|
+
policyMetamodelPackage
|
|
14824
15822
|
];
|
|
14825
15823
|
var DEFAULT_METAMODEL_REGISTRY = createMetamodelRegistry(
|
|
14826
15824
|
DEFAULT_METAMODEL_PACKAGES
|
|
@@ -15126,7 +16124,112 @@ Example:
|
|
|
15126
16124
|
|
|
15127
16125
|
**Lifecycle:** declare in manifest \u2192 \`granular build\` creates or reuses a version and runs the build \u2192 implement handlers \u2192 \`granular.ontology(sandboxId).effects.registerMany([...])\` \u2192 jobs call generated methods \u2192 your handler runs and returns the result to the job.
|
|
15128
16126
|
|
|
15129
|
-
**Permissions:** \`openEnvironment({ permissions: [...] })\` assigns
|
|
16127
|
+
**Permissions:** \`openEnvironment({ permissions: [...] })\` assigns one permission profile for the environment/session. Profile files live in \`permissions/*.json\`, are synced by \`granular build\` / \`granular deploy\`, are compiled into the ontology version, and decide whether each generated action is visible, allowed, confirmed, or denied.
|
|
16128
|
+
|
|
16129
|
+
### Manifest logic policies
|
|
16130
|
+
|
|
16131
|
+
Use \`withEffect.metamodels.policies\` for product-wide guardrails that apply to every profile. Manifest policies are strongest for rules such as \u201Clocked objects cannot be changed\u201D or \u201Cexternal payments need confirmation.\u201D
|
|
16132
|
+
|
|
16133
|
+
\`\`\`json
|
|
16134
|
+
{
|
|
16135
|
+
"withEffect": {
|
|
16136
|
+
"name": "approve_ticket",
|
|
16137
|
+
"attachedClass": "ticket",
|
|
16138
|
+
"inputSchema": { "type": "object", "properties": { "note": { "type": "string" } } },
|
|
16139
|
+
"outputSchema": { "type": "object", "properties": { "ok": { "type": "boolean" } } },
|
|
16140
|
+
"metamodels": {
|
|
16141
|
+
"policies": {
|
|
16142
|
+
"denyWhen": [
|
|
16143
|
+
{
|
|
16144
|
+
"id": "locked-ticket-deny",
|
|
16145
|
+
"reason": "Locked tickets cannot be approved",
|
|
16146
|
+
"when": {
|
|
16147
|
+
"object": { "path": "locked", "operator": "eq", "booleanValue": true }
|
|
16148
|
+
}
|
|
16149
|
+
}
|
|
16150
|
+
],
|
|
16151
|
+
"confirmWhen": [
|
|
16152
|
+
{
|
|
16153
|
+
"id": "high-risk-confirm",
|
|
16154
|
+
"reason": "High-risk active tickets require confirmation",
|
|
16155
|
+
"when": {
|
|
16156
|
+
"all": [
|
|
16157
|
+
{ "object": { "path": "risk_score", "operator": "gt", "numberValue": 50 } },
|
|
16158
|
+
{ "stateMachine": { "machine": "lifecycle", "operator": "eq", "stringValue": "active" } }
|
|
16159
|
+
]
|
|
16160
|
+
}
|
|
16161
|
+
}
|
|
16162
|
+
]
|
|
16163
|
+
}
|
|
16164
|
+
}
|
|
16165
|
+
}
|
|
16166
|
+
}
|
|
16167
|
+
\`\`\`
|
|
16168
|
+
|
|
16169
|
+
### Permission profile files
|
|
16170
|
+
|
|
16171
|
+
Each file under \`permissions/\` defines one role/profile. Built-ins are \`allow-all\`, \`confirm-all\`, and \`deny-all\`; \`granular init\` and \`granular pull\` materialize them as files.
|
|
16172
|
+
|
|
16173
|
+
\`\`\`json
|
|
16174
|
+
{
|
|
16175
|
+
"schemaVersion": 1,
|
|
16176
|
+
"name": "reviewer",
|
|
16177
|
+
"description": "Reviewer can comment and conditionally approve tickets",
|
|
16178
|
+
"defaults": { "actionPolicy": "deny" },
|
|
16179
|
+
"policies": [
|
|
16180
|
+
{
|
|
16181
|
+
"id": "reviewer-low-risk-allow",
|
|
16182
|
+
"action": "approve_ticket",
|
|
16183
|
+
"on": "ticket",
|
|
16184
|
+
"decision": "allow",
|
|
16185
|
+
"reason": "Active low-risk tickets can be approved by reviewers",
|
|
16186
|
+
"when": {
|
|
16187
|
+
"all": [
|
|
16188
|
+
{ "object": { "path": "risk_score", "operator": "lte", "numberValue": 50 } },
|
|
16189
|
+
{ "stateMachine": { "machine": "lifecycle", "operator": "eq", "stringValue": "active" } },
|
|
16190
|
+
{ "not": { "object": { "path": "locked", "operator": "eq", "booleanValue": true } } }
|
|
16191
|
+
]
|
|
16192
|
+
}
|
|
16193
|
+
}
|
|
16194
|
+
],
|
|
16195
|
+
"actions": [
|
|
16196
|
+
{
|
|
16197
|
+
"action": "comment_ticket",
|
|
16198
|
+
"on": "ticket",
|
|
16199
|
+
"allow": "always",
|
|
16200
|
+
"reason": "Reviewers can comment on tickets"
|
|
16201
|
+
}
|
|
16202
|
+
]
|
|
16203
|
+
}
|
|
16204
|
+
\`\`\`
|
|
16205
|
+
|
|
16206
|
+
Profile rules:
|
|
16207
|
+
|
|
16208
|
+
- \`defaults.actionPolicy\` is \`allow\`, \`confirm\`, or \`deny\`; omitted means \`deny\`.
|
|
16209
|
+
- \`policies[]\` can target one action with \`action\` + optional \`on\`, or all declared actions if \`action\` is omitted.
|
|
16210
|
+
- \`actions[]\` supports \`allow: "always"\`, \`confirm: "always"\`, \`deny: "always"\`, \`allowWhen\`, \`confirmWhen\`, \`denyWhen\`, and numeric \`limits\`.
|
|
16211
|
+
- \`limits\` is a shorthand over numeric input fields: \`{ "input": "amount", "upTo": 500, "decision": "allow" }\` and \`{ "input": "amount", "above": 5000, "decision": "deny" }\`.
|
|
16212
|
+
- \`extends\` inherits exactly one parent profile. A child profile cannot define its own \`defaults\`; it can add rules and override action rules by the same \`{ action, on }\`.
|
|
16213
|
+
|
|
16214
|
+
Condition sources:
|
|
16215
|
+
|
|
16216
|
+
- \`input\`: the action input passed by job code.
|
|
16217
|
+
- \`object\`: fields on the target object for instance effects.
|
|
16218
|
+
- \`stateMachine\`: current state of a workflow attached to the target object.
|
|
16219
|
+
- Logical composition: \`all\`, \`any\`, and \`not\`.
|
|
16220
|
+
|
|
16221
|
+
Decision precedence is deterministic: manifest \`deny\`, profile \`deny\`, manifest/profile \`confirm\`, profile \`allow\`, then the profile default. Runtime blocks \`deny\` before provider invocation and asks a human confirmation for \`confirm\`.
|
|
16222
|
+
|
|
16223
|
+
Useful commands:
|
|
16224
|
+
|
|
16225
|
+
\`\`\`bash
|
|
16226
|
+
granular permissions validate
|
|
16227
|
+
granular permissions preview --profile reviewer --action approve_ticket --on ticket \\
|
|
16228
|
+
--input '{"note":"ok"}' \\
|
|
16229
|
+
--object '{"risk_score":40,"locked":false}' \\
|
|
16230
|
+
--state-machines '{"lifecycle":"active"}'
|
|
16231
|
+
granular build
|
|
16232
|
+
\`\`\`
|
|
15130
16233
|
|
|
15131
16234
|
### Effect metamodels
|
|
15132
16235
|
|
|
@@ -15247,6 +16350,8 @@ Use \`environment.graphql(query, variables?)\` when you need **query/mutation ac
|
|
|
15247
16350
|
| \`granular build\` | Upload \`granular.json\`, create or reuse the matching ontology version, and run the build (**validation**). |
|
|
15248
16351
|
| \`granular deploy\` | Push the current revision, build it, and update \`dev\`. |
|
|
15249
16352
|
| \`granular deploy --prod\` | If the current version is already on \`dev\`, also point \`prod\` to it. Otherwise build it, then point both \`dev\` and \`prod\` to it. |
|
|
16353
|
+
| \`granular permissions validate\` | Validate \`permissions/*.json\` against local \`granular.json\`, including action references, inheritance, limits, and conditions. |
|
|
16354
|
+
| \`granular permissions preview --profile <name> --action <name> [--on <class>] [--input '{}'] [--object '{}'] [--state-machines '{}'] [--version-id <id>]\` | Preview the exact \`allow\` / \`confirm\` / \`deny\` decision locally or against a compiled version. |
|
|
15250
16355
|
| \`granular version current --json\` | Resolve the latest/current ontology version for agent workflows. |
|
|
15251
16356
|
| \`granular version diff\` | Show the semantic diff between ontology versions. |
|
|
15252
16357
|
| \`granular tag move\` | Move \`dev\`, \`prod\`, or another tag to a chosen version. |
|
|
@@ -15261,7 +16366,7 @@ Use \`environment.graphql(query, variables?)\` when you need **query/mutation ac
|
|
|
15261
16366
|
| \`granular graphql --query '...' --json\` | Run a GraphQL query or mutation against the live graph. |
|
|
15262
16367
|
| \`granular effects list/diff --json\` | Inspect declared versus live ready effects. |
|
|
15263
16368
|
| \`granular job run --file ./job.ts --json\` | Execute a real runtime job from the terminal or CI. |
|
|
15264
|
-
| \`granular pull <version>\` | Pull the manifest for a specific ontology version. |
|
|
16369
|
+
| \`granular pull <version>\` | Pull the manifest and versioned \`permissions/*.json\` files for a specific ontology version. |
|
|
15265
16370
|
| \`granular login\`, \`granular whoami\`, \`granular status --verbose --json\` | Auth, config, and project info. |
|
|
15266
16371
|
|
|
15267
16372
|
---
|
|
@@ -15428,7 +16533,7 @@ function generateSandboxAgentDoc(manifest, meta) {
|
|
|
15428
16533
|
lines.push(` ontology: '${meta.sandboxId}',`);
|
|
15429
16534
|
lines.push(` tag: 'dev',`);
|
|
15430
16535
|
lines.push(` userId: 'your_app_user_id',`);
|
|
15431
|
-
lines.push(` permissions: ['
|
|
16536
|
+
lines.push(` permissions: ['allow-all'],`);
|
|
15432
16537
|
lines.push(`});`);
|
|
15433
16538
|
lines.push("```");
|
|
15434
16539
|
lines.push("");
|
|
@@ -15978,7 +17083,7 @@ Use this when proving that the ontology works in real runtime conditions.
|
|
|
15978
17083
|
|
|
15979
17084
|
- Load \`GRANULAR_API_KEY\` from environment when running unattended tests.
|
|
15980
17085
|
- Read the ontology id from \`.granularrc\` if the repo uses \`granular init\`.
|
|
15981
|
-
- Connect with \`new Granular({ apiKey }).connect({ ontology, environment: 'dev', userId, permissions: ['
|
|
17086
|
+
- Connect with \`new Granular({ apiKey }).connect({ ontology, environment: 'dev', userId, permissions: ['allow-all'] })\`.
|
|
15982
17087
|
- Assert on real behavior rather than giant snapshots.
|
|
15983
17088
|
|
|
15984
17089
|
# What to verify
|
|
@@ -16725,9 +17830,11 @@ async function initCommand(projectName, options) {
|
|
|
16725
17830
|
}
|
|
16726
17831
|
const project = createDefaultManifest(name, templateId);
|
|
16727
17832
|
writeManifestFile(project);
|
|
17833
|
+
ensureBuiltinPermissionFiles();
|
|
16728
17834
|
success(
|
|
16729
17835
|
`Created ${brand.bold("granular.json")} with the ${template.label} starter ontology`
|
|
16730
17836
|
);
|
|
17837
|
+
success(`Created ${brand.bold("permissions/")} with built-in profiles`);
|
|
16731
17838
|
writeRcFile({
|
|
16732
17839
|
sandboxId: sandbox.sandboxId,
|
|
16733
17840
|
sandboxName: sandbox.name,
|
|
@@ -17060,8 +18167,32 @@ async function loginCommand(options = {}) {
|
|
|
17060
18167
|
dim("API key saved to .env.local (used by CLI and SDK examples)");
|
|
17061
18168
|
console.log();
|
|
17062
18169
|
}
|
|
17063
|
-
|
|
17064
|
-
|
|
18170
|
+
function cleanPermissionProfile(profile) {
|
|
18171
|
+
const {
|
|
18172
|
+
source,
|
|
18173
|
+
sourcePath,
|
|
18174
|
+
defaultsOwnerName,
|
|
18175
|
+
...rest
|
|
18176
|
+
} = profile;
|
|
18177
|
+
return rest;
|
|
18178
|
+
}
|
|
18179
|
+
function writePermissionProfileFiles(profiles) {
|
|
18180
|
+
const permissionsDir = getPermissionsDir();
|
|
18181
|
+
fs__namespace.mkdirSync(permissionsDir, { recursive: true });
|
|
18182
|
+
const written = [];
|
|
18183
|
+
for (const profile of profiles) {
|
|
18184
|
+
if (!profile?.name) continue;
|
|
18185
|
+
const fileName = `${profile.name}.json`;
|
|
18186
|
+
const filePath = path3__namespace.join(permissionsDir, fileName);
|
|
18187
|
+
fs__namespace.writeFileSync(
|
|
18188
|
+
filePath,
|
|
18189
|
+
JSON.stringify(cleanPermissionProfile(profile), null, 2) + "\n",
|
|
18190
|
+
"utf-8"
|
|
18191
|
+
);
|
|
18192
|
+
written.push(path3__namespace.join("permissions", fileName));
|
|
18193
|
+
}
|
|
18194
|
+
return written.sort();
|
|
18195
|
+
}
|
|
17065
18196
|
async function pullCommand(versionId) {
|
|
17066
18197
|
printHeader();
|
|
17067
18198
|
const config = resolveConfig({ requireApiKey: true });
|
|
@@ -17085,13 +18216,18 @@ async function pullCommand(versionId) {
|
|
|
17085
18216
|
manifest: full.content
|
|
17086
18217
|
};
|
|
17087
18218
|
writeManifestFile(project);
|
|
18219
|
+
spin.text = " Downloading permission profile files...";
|
|
18220
|
+
const profileVersions = await api.getVersionPermissionProfiles(versionId);
|
|
18221
|
+
const pulledProfiles = (profileVersions.items || []).map((item) => item.profile).filter((profile) => Boolean(profile?.name));
|
|
18222
|
+
const writtenProfiles = writePermissionProfileFiles(pulledProfiles);
|
|
17088
18223
|
spin.succeed(" Manifest pulled successfully.");
|
|
17089
18224
|
console.log();
|
|
17090
18225
|
keyValue({
|
|
17091
18226
|
"Manifest ID": full.manifestId,
|
|
17092
18227
|
"Source Version": versionId,
|
|
17093
18228
|
"Version": targetVersionLabel,
|
|
17094
|
-
"Written to": "granular.json"
|
|
18229
|
+
"Written to": "granular.json",
|
|
18230
|
+
"Permission profiles": writtenProfiles.length > 0 ? writtenProfiles.join(", ") : "none"
|
|
17095
18231
|
});
|
|
17096
18232
|
console.log();
|
|
17097
18233
|
} catch (err) {
|
|
@@ -17110,6 +18246,28 @@ async function buildCommand() {
|
|
|
17110
18246
|
}
|
|
17111
18247
|
const api = new ApiClient(config.apiKey, config.apiUrl);
|
|
17112
18248
|
const manifest = config.project.manifest;
|
|
18249
|
+
const permissionProfiles = readPermissionProfileFiles();
|
|
18250
|
+
const localPolicyValidation = await compilePolicyBundle({
|
|
18251
|
+
sandboxId: config.sandboxId,
|
|
18252
|
+
manifestDigest: void 0,
|
|
18253
|
+
effects: extractManifestEffects(manifest),
|
|
18254
|
+
profileSources: permissionProfiles
|
|
18255
|
+
});
|
|
18256
|
+
if (localPolicyValidation.errors.length > 0) {
|
|
18257
|
+
error("Permission policy validation failed:");
|
|
18258
|
+
for (const error2 of localPolicyValidation.errors) {
|
|
18259
|
+
error(` ${error2}`);
|
|
18260
|
+
}
|
|
18261
|
+
process.exit(1);
|
|
18262
|
+
}
|
|
18263
|
+
const syncingProfiles = spinner(`Syncing ${permissionProfiles.length} permission profile file(s)...`);
|
|
18264
|
+
try {
|
|
18265
|
+
const result = await api.syncPermissionProfileSources(config.sandboxId, permissionProfiles);
|
|
18266
|
+
syncingProfiles.succeed(` Permission profiles synced: ${brand.secondary(result.digest)}`);
|
|
18267
|
+
} catch (err) {
|
|
18268
|
+
syncingProfiles.fail(` Failed to sync permission profiles: ${err.message}`);
|
|
18269
|
+
process.exit(1);
|
|
18270
|
+
}
|
|
17113
18271
|
const uploading = spinner(`Uploading manifest "${manifest.name}"...`);
|
|
17114
18272
|
let uploadedManifest;
|
|
17115
18273
|
try {
|
|
@@ -17184,6 +18342,25 @@ async function deployCommand(options = {}) {
|
|
|
17184
18342
|
step(options.prod ? "Deploy to dev and prod" : "Deploy to dev", `Sandbox ${config.sandboxId}`);
|
|
17185
18343
|
console.log();
|
|
17186
18344
|
const manifest = config.project.manifest;
|
|
18345
|
+
const permissionProfiles = readPermissionProfileFiles();
|
|
18346
|
+
const localPolicyValidation = await compilePolicyBundle({
|
|
18347
|
+
sandboxId: config.sandboxId,
|
|
18348
|
+
effects: extractManifestEffects(manifest),
|
|
18349
|
+
profileSources: permissionProfiles
|
|
18350
|
+
});
|
|
18351
|
+
if (localPolicyValidation.errors.length > 0) {
|
|
18352
|
+
error("Permission policy validation failed:");
|
|
18353
|
+
for (const error2 of localPolicyValidation.errors) error(` ${error2}`);
|
|
18354
|
+
process.exit(1);
|
|
18355
|
+
}
|
|
18356
|
+
const syncingProfiles = spinner(`Syncing ${permissionProfiles.length} permission profile file(s)...`);
|
|
18357
|
+
try {
|
|
18358
|
+
const result = await api.syncPermissionProfileSources(config.sandboxId, permissionProfiles);
|
|
18359
|
+
syncingProfiles.succeed(` Permission profiles synced: ${brand.secondary(result.digest)}`);
|
|
18360
|
+
} catch (err) {
|
|
18361
|
+
syncingProfiles.fail(` Failed to sync permission profiles: ${err.message}`);
|
|
18362
|
+
process.exit(1);
|
|
18363
|
+
}
|
|
17187
18364
|
const uploading = spinner(`Uploading manifest "${manifest.name}"...`);
|
|
17188
18365
|
let uploadedManifest;
|
|
17189
18366
|
try {
|
|
@@ -17726,11 +18903,22 @@ var TOKEN_REFRESH_LEEWAY_MS = 2 * 60 * 1e3;
|
|
|
17726
18903
|
var TOKEN_REFRESH_RETRY_MS = 30 * 1e3;
|
|
17727
18904
|
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
17728
18905
|
var DEBUG_WS = process.env.GRANULAR_DEBUG_WS === "1";
|
|
18906
|
+
var DEFAULT_RPC_TIMEOUT_MS = 3e4;
|
|
18907
|
+
var DOMAIN_PACKAGE_RPC_TIMEOUT_MS = 12e4;
|
|
17729
18908
|
function debugWs(...args) {
|
|
17730
18909
|
if (DEBUG_WS) {
|
|
17731
18910
|
console.log(...args);
|
|
17732
18911
|
}
|
|
17733
18912
|
}
|
|
18913
|
+
function rpcTimeoutMsForMethod(method) {
|
|
18914
|
+
switch (method) {
|
|
18915
|
+
case "domain.fetchPackagePart":
|
|
18916
|
+
case "domain.getSummary":
|
|
18917
|
+
return DOMAIN_PACKAGE_RPC_TIMEOUT_MS;
|
|
18918
|
+
default:
|
|
18919
|
+
return DEFAULT_RPC_TIMEOUT_MS;
|
|
18920
|
+
}
|
|
18921
|
+
}
|
|
17734
18922
|
var WSClient = class {
|
|
17735
18923
|
ws = null;
|
|
17736
18924
|
url;
|
|
@@ -18155,13 +19343,14 @@ var WSClient = class {
|
|
|
18155
19343
|
return new Promise((resolve2, reject) => {
|
|
18156
19344
|
this.messageQueue.push({ resolve: resolve2, reject, id });
|
|
18157
19345
|
this.ws.send(JSON.stringify(request));
|
|
19346
|
+
const timeoutMs = rpcTimeoutMsForMethod(method);
|
|
18158
19347
|
setTimeout(() => {
|
|
18159
19348
|
const pending = this.messageQueue.find((q) => q.id === id);
|
|
18160
19349
|
if (pending) {
|
|
18161
19350
|
this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
|
|
18162
19351
|
reject(new Error(`RPC timeout: ${method}`));
|
|
18163
19352
|
}
|
|
18164
|
-
},
|
|
19353
|
+
}, timeoutMs);
|
|
18165
19354
|
});
|
|
18166
19355
|
}
|
|
18167
19356
|
async handleIncomingRpc(request) {
|
|
@@ -18275,10 +19464,48 @@ function normalizePromptText(value) {
|
|
|
18275
19464
|
function extractPromptTokens(value) {
|
|
18276
19465
|
return normalizePromptText(value).split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
|
|
18277
19466
|
}
|
|
19467
|
+
function parseJsonPromptChoiceOption(option) {
|
|
19468
|
+
const trimmed = option.trim();
|
|
19469
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
|
19470
|
+
try {
|
|
19471
|
+
const parsed = JSON.parse(trimmed);
|
|
19472
|
+
return asRecord(parsed);
|
|
19473
|
+
} catch {
|
|
19474
|
+
return null;
|
|
19475
|
+
}
|
|
19476
|
+
}
|
|
19477
|
+
function normalizePromptChoiceOption(option) {
|
|
19478
|
+
if (typeof option === "string") {
|
|
19479
|
+
const record2 = parseJsonPromptChoiceOption(option);
|
|
19480
|
+
if (!record2) {
|
|
19481
|
+
return { value: option, label: option };
|
|
19482
|
+
}
|
|
19483
|
+
const value2 = typeof record2.value === "string" ? record2.value : typeof record2.id === "string" ? record2.id : typeof record2.label === "string" ? record2.label : JSON.stringify(record2);
|
|
19484
|
+
return {
|
|
19485
|
+
value: value2,
|
|
19486
|
+
label: typeof record2.label === "string" ? record2.label : value2,
|
|
19487
|
+
description: typeof record2.description === "string" ? record2.description : void 0
|
|
19488
|
+
};
|
|
19489
|
+
}
|
|
19490
|
+
const record = option;
|
|
19491
|
+
if (!record) {
|
|
19492
|
+
return { value: "", label: "" };
|
|
19493
|
+
}
|
|
19494
|
+
const nestedJson = (typeof record.value === "string" ? parseJsonPromptChoiceOption(record.value) : null) || (typeof record.label === "string" ? parseJsonPromptChoiceOption(record.label) : null);
|
|
19495
|
+
if (nestedJson) {
|
|
19496
|
+
return normalizePromptChoiceOption(nestedJson);
|
|
19497
|
+
}
|
|
19498
|
+
const value = typeof record.value === "string" ? record.value : typeof record.label === "string" ? record.label : JSON.stringify(record);
|
|
19499
|
+
return {
|
|
19500
|
+
value,
|
|
19501
|
+
label: typeof record.label === "string" ? record.label : value,
|
|
19502
|
+
description: typeof record.description === "string" ? record.description : void 0
|
|
19503
|
+
};
|
|
19504
|
+
}
|
|
18278
19505
|
function scorePromptChoiceMatch(answer, answerTokens, option) {
|
|
18279
|
-
const
|
|
18280
|
-
const
|
|
18281
|
-
const description =
|
|
19506
|
+
const choice = normalizePromptChoiceOption(option);
|
|
19507
|
+
const { value, label } = choice;
|
|
19508
|
+
const description = choice.description || "";
|
|
18282
19509
|
const haystack = normalizePromptText([value, label, description].filter(Boolean).join(" "));
|
|
18283
19510
|
if (!haystack) return { score: 0, resolvedValue: value || label || null };
|
|
18284
19511
|
let score = 0;
|
|
@@ -18314,7 +19541,9 @@ function normalizePrompt(rawValue) {
|
|
|
18314
19541
|
type: normalizePromptType(source === raw ? raw : { ...raw, ...source }),
|
|
18315
19542
|
title: typeof source.title === "string" ? source.title : "Input required",
|
|
18316
19543
|
message: typeof source.message === "string" ? source.message : "",
|
|
18317
|
-
options: Array.isArray(source.options) ? source.options
|
|
19544
|
+
options: Array.isArray(source.options) ? source.options.map(
|
|
19545
|
+
(option) => typeof option === "string" || asRecord(option) ? normalizePromptChoiceOption(option) : option
|
|
19546
|
+
) : void 0,
|
|
18318
19547
|
defaultValue: source.defaultValue,
|
|
18319
19548
|
placeholder: typeof source.placeholder === "string" ? source.placeholder : void 0,
|
|
18320
19549
|
allowEmpty: typeof source.allowEmpty === "boolean" ? source.allowEmpty : void 0,
|
|
@@ -18342,9 +19571,26 @@ function resolvePromptAnswer(prompt3, answer) {
|
|
|
18342
19571
|
}
|
|
18343
19572
|
|
|
18344
19573
|
// src/session.ts
|
|
19574
|
+
var PROMPT_TRANSCRIPT_APPEND_TIMEOUT_MS = 5e3;
|
|
19575
|
+
function withPromptTranscriptTimeout(promise) {
|
|
19576
|
+
let timeout = null;
|
|
19577
|
+
return Promise.race([
|
|
19578
|
+
promise,
|
|
19579
|
+
new Promise((_, reject) => {
|
|
19580
|
+
timeout = setTimeout(() => {
|
|
19581
|
+
reject(new Error("Timed out appending prompt answer transcript."));
|
|
19582
|
+
}, PROMPT_TRANSCRIPT_APPEND_TIMEOUT_MS);
|
|
19583
|
+
})
|
|
19584
|
+
]).finally(() => {
|
|
19585
|
+
if (timeout) {
|
|
19586
|
+
clearTimeout(timeout);
|
|
19587
|
+
}
|
|
19588
|
+
});
|
|
19589
|
+
}
|
|
18345
19590
|
var Session = class {
|
|
18346
19591
|
client;
|
|
18347
19592
|
clientId;
|
|
19593
|
+
initialQuota;
|
|
18348
19594
|
jobsMap = /* @__PURE__ */ new Map();
|
|
18349
19595
|
pendingAgentMessagesByJobId = /* @__PURE__ */ new Map();
|
|
18350
19596
|
eventListeners = /* @__PURE__ */ new Map();
|
|
@@ -18358,9 +19604,12 @@ var Session = class {
|
|
|
18358
19604
|
lastKnownTools = /* @__PURE__ */ new Map();
|
|
18359
19605
|
/** Last seen live prompts, keyed by prompt id, for answer normalization */
|
|
18360
19606
|
promptCache = /* @__PURE__ */ new Map();
|
|
18361
|
-
|
|
19607
|
+
/** Prompt ids locally answered before the document sync catches up. */
|
|
19608
|
+
hiddenPromptIds = /* @__PURE__ */ new Set();
|
|
19609
|
+
constructor(client, clientId, options = {}) {
|
|
18362
19610
|
this.client = client;
|
|
18363
19611
|
this.clientId = clientId || `client_${Date.now()}`;
|
|
19612
|
+
this.initialQuota = options.initialQuota || null;
|
|
18364
19613
|
this.setupEventHandlers();
|
|
18365
19614
|
this.setupToolInvokeHandler();
|
|
18366
19615
|
}
|
|
@@ -18409,6 +19658,16 @@ var Session = class {
|
|
|
18409
19658
|
get document() {
|
|
18410
19659
|
return this.client.doc;
|
|
18411
19660
|
}
|
|
19661
|
+
get quota() {
|
|
19662
|
+
return this.getQuota();
|
|
19663
|
+
}
|
|
19664
|
+
getQuota() {
|
|
19665
|
+
const quota = this.client.doc.billing?.quota;
|
|
19666
|
+
if (quota && typeof quota === "object") {
|
|
19667
|
+
return quota;
|
|
19668
|
+
}
|
|
19669
|
+
return this.initialQuota;
|
|
19670
|
+
}
|
|
18412
19671
|
get sessionId() {
|
|
18413
19672
|
return this.client.currentSessionId;
|
|
18414
19673
|
}
|
|
@@ -18493,8 +19752,9 @@ var Session = class {
|
|
|
18493
19752
|
* `effect.invoke` RPC back to the sandbox effect host, where the registered handlers
|
|
18494
19753
|
* execute locally and return the result to the sandbox.
|
|
18495
19754
|
*/
|
|
18496
|
-
async submitJob(code,
|
|
18497
|
-
|
|
19755
|
+
async submitJob(code, domainRevisionOrOptions) {
|
|
19756
|
+
const options = typeof domainRevisionOrOptions === "string" ? { domainRevision: domainRevisionOrOptions } : domainRevisionOrOptions || {};
|
|
19757
|
+
let revision = options.domainRevision || this.currentDomainRevision || this.extractDomainRevisionFromDoc(this.client.doc) || void 0;
|
|
18498
19758
|
if (!revision) {
|
|
18499
19759
|
try {
|
|
18500
19760
|
const summary = await this.getDomain();
|
|
@@ -18509,7 +19769,9 @@ var Session = class {
|
|
|
18509
19769
|
}
|
|
18510
19770
|
const result = await this.client.call("job.submit", {
|
|
18511
19771
|
domainRevision: revision,
|
|
18512
|
-
code
|
|
19772
|
+
code,
|
|
19773
|
+
metadata: options.metadata,
|
|
19774
|
+
agent: options.agent
|
|
18513
19775
|
});
|
|
18514
19776
|
if (!result.jobId) {
|
|
18515
19777
|
throw new Error("Failed to submit job: no jobId returned");
|
|
@@ -18550,25 +19812,39 @@ var Session = class {
|
|
|
18550
19812
|
const prompt3 = this.promptCache.get(promptId);
|
|
18551
19813
|
const resolvedAnswer = resolvePromptAnswer(prompt3, answer);
|
|
18552
19814
|
this.promptCache.delete(promptId);
|
|
18553
|
-
|
|
18554
|
-
|
|
18555
|
-
answer
|
|
18556
|
-
|
|
18557
|
-
|
|
19815
|
+
this.hiddenPromptIds.add(promptId);
|
|
19816
|
+
try {
|
|
19817
|
+
await this.client.call("prompt.answer", {
|
|
19818
|
+
promptId,
|
|
19819
|
+
answer: resolvedAnswer,
|
|
19820
|
+
value: resolvedAnswer
|
|
19821
|
+
});
|
|
19822
|
+
} catch (error2) {
|
|
19823
|
+
this.hiddenPromptIds.delete(promptId);
|
|
19824
|
+
if (prompt3) {
|
|
19825
|
+
this.promptCache.set(promptId, prompt3);
|
|
19826
|
+
}
|
|
19827
|
+
throw error2;
|
|
19828
|
+
}
|
|
18558
19829
|
try {
|
|
18559
19830
|
const content = this.stringifyConversationValue(resolvedAnswer);
|
|
18560
19831
|
if (content.trim()) {
|
|
18561
|
-
await
|
|
18562
|
-
|
|
18563
|
-
|
|
18564
|
-
|
|
18565
|
-
|
|
19832
|
+
await withPromptTranscriptTimeout(
|
|
19833
|
+
this.appendConversationMessage({
|
|
19834
|
+
role: "user",
|
|
19835
|
+
content,
|
|
19836
|
+
promptId
|
|
19837
|
+
})
|
|
19838
|
+
);
|
|
18566
19839
|
}
|
|
18567
19840
|
} catch {
|
|
18568
19841
|
}
|
|
18569
19842
|
}
|
|
18570
19843
|
async appendConversationMessage(input) {
|
|
18571
|
-
return this.client.call(
|
|
19844
|
+
return this.client.call(
|
|
19845
|
+
"conversation.append",
|
|
19846
|
+
input
|
|
19847
|
+
);
|
|
18572
19848
|
}
|
|
18573
19849
|
/**
|
|
18574
19850
|
* Get the current list of available effects.
|
|
@@ -18577,9 +19853,53 @@ var Session = class {
|
|
|
18577
19853
|
getEffects() {
|
|
18578
19854
|
const doc = this.client.doc;
|
|
18579
19855
|
const toolMap = /* @__PURE__ */ new Map();
|
|
18580
|
-
const
|
|
18581
|
-
|
|
18582
|
-
|
|
19856
|
+
const domainPackages = doc.domain?.packages;
|
|
19857
|
+
const packageCandidates = domainPackages && typeof domainPackages === "object" ? [
|
|
19858
|
+
domainPackages.domain,
|
|
19859
|
+
domainPackages["@sandbox/domain"],
|
|
19860
|
+
...Object.values(domainPackages)
|
|
19861
|
+
].filter(Boolean) : [];
|
|
19862
|
+
for (const domainPkg of packageCandidates) {
|
|
19863
|
+
if (domainPkg?.tools && Array.isArray(domainPkg.tools)) {
|
|
19864
|
+
for (const tool of domainPkg.tools) {
|
|
19865
|
+
if (!tool?.name || toolMap.has(tool.name)) continue;
|
|
19866
|
+
toolMap.set(tool.name, {
|
|
19867
|
+
name: tool.name,
|
|
19868
|
+
description: tool.description,
|
|
19869
|
+
inputSchema: tool.inputSchema,
|
|
19870
|
+
outputSchema: tool.outputSchema,
|
|
19871
|
+
className: tool.className || void 0,
|
|
19872
|
+
static: tool.static || false,
|
|
19873
|
+
ready: false,
|
|
19874
|
+
publishedAt: void 0
|
|
19875
|
+
});
|
|
19876
|
+
}
|
|
19877
|
+
}
|
|
19878
|
+
if (!domainPkg?.classes || typeof domainPkg.classes !== "object") {
|
|
19879
|
+
continue;
|
|
19880
|
+
}
|
|
19881
|
+
for (const [className, classDef] of Object.entries(
|
|
19882
|
+
domainPkg.classes
|
|
19883
|
+
)) {
|
|
19884
|
+
const methods = Array.isArray(classDef?.methods) ? classDef.methods : [];
|
|
19885
|
+
for (const method of methods) {
|
|
19886
|
+
if (!method?.name || toolMap.has(method.name)) continue;
|
|
19887
|
+
toolMap.set(method.name, {
|
|
19888
|
+
name: method.name,
|
|
19889
|
+
description: method.description,
|
|
19890
|
+
inputSchema: method.inputSchema,
|
|
19891
|
+
outputSchema: method.outputSchema,
|
|
19892
|
+
className: method.className || classDef?.name || className,
|
|
19893
|
+
static: method.static || false,
|
|
19894
|
+
ready: false,
|
|
19895
|
+
publishedAt: void 0
|
|
19896
|
+
});
|
|
19897
|
+
}
|
|
19898
|
+
}
|
|
19899
|
+
}
|
|
19900
|
+
const legacyDomainPkg = doc.domain?.packages?.domain;
|
|
19901
|
+
if (legacyDomainPkg?.tools && Array.isArray(legacyDomainPkg.tools)) {
|
|
19902
|
+
for (const tool of legacyDomainPkg.tools) {
|
|
18583
19903
|
if (!tool?.name) continue;
|
|
18584
19904
|
toolMap.set(tool.name, {
|
|
18585
19905
|
name: tool.name,
|
|
@@ -18593,6 +19913,27 @@ var Session = class {
|
|
|
18593
19913
|
});
|
|
18594
19914
|
}
|
|
18595
19915
|
}
|
|
19916
|
+
if (legacyDomainPkg?.classes && typeof legacyDomainPkg.classes === "object") {
|
|
19917
|
+
for (const [className, classDef] of Object.entries(
|
|
19918
|
+
legacyDomainPkg.classes
|
|
19919
|
+
)) {
|
|
19920
|
+
const methods = Array.isArray(classDef?.methods) ? classDef.methods : [];
|
|
19921
|
+
for (const method of methods) {
|
|
19922
|
+
if (!method?.name || toolMap.has(method.name)) continue;
|
|
19923
|
+
toolMap.set(method.name, {
|
|
19924
|
+
name: method.name,
|
|
19925
|
+
description: method.description,
|
|
19926
|
+
inputSchema: method.inputSchema,
|
|
19927
|
+
outputSchema: method.outputSchema,
|
|
19928
|
+
className: method.className || classDef?.name || className,
|
|
19929
|
+
static: method.static || false,
|
|
19930
|
+
ready: false,
|
|
19931
|
+
publishedAt: void 0
|
|
19932
|
+
});
|
|
19933
|
+
}
|
|
19934
|
+
}
|
|
19935
|
+
}
|
|
19936
|
+
const hasPolicyFilteredDomainTools = toolMap.size > 0;
|
|
18596
19937
|
const catalogs = doc.catalog?.rawToolCatalogs || {};
|
|
18597
19938
|
for (const [clientId, catalog] of Object.entries(catalogs)) {
|
|
18598
19939
|
const cat = catalog;
|
|
@@ -18600,6 +19941,7 @@ var Session = class {
|
|
|
18600
19941
|
for (const tool of cat.tools) {
|
|
18601
19942
|
if (!tool?.name) continue;
|
|
18602
19943
|
const existing = toolMap.get(tool.name);
|
|
19944
|
+
if (hasPolicyFilteredDomainTools && !existing) continue;
|
|
18603
19945
|
if (existing?.publishedAt && cat.publishedAt && existing.publishedAt > cat.publishedAt)
|
|
18604
19946
|
continue;
|
|
18605
19947
|
const isLocal = clientId === this.clientId;
|
|
@@ -18619,6 +19961,24 @@ var Session = class {
|
|
|
18619
19961
|
}
|
|
18620
19962
|
return Array.from(toolMap.values());
|
|
18621
19963
|
}
|
|
19964
|
+
/**
|
|
19965
|
+
* Return the currently open prompt payloads known to this session.
|
|
19966
|
+
*
|
|
19967
|
+
* These come from live `prompt` / `prompt.request` websocket events and
|
|
19968
|
+
* preserve the exact shape used by `answerPrompt(...)`.
|
|
19969
|
+
*/
|
|
19970
|
+
getPrompts() {
|
|
19971
|
+
return Array.from(this.promptCache.values()).map((prompt3) => ({
|
|
19972
|
+
...prompt3,
|
|
19973
|
+
options: Array.isArray(prompt3.options) ? prompt3.options.map(
|
|
19974
|
+
(option) => typeof option === "string" ? option : { ...option }
|
|
19975
|
+
) : void 0,
|
|
19976
|
+
metadata: prompt3.metadata ? { ...prompt3.metadata } : void 0
|
|
19977
|
+
}));
|
|
19978
|
+
}
|
|
19979
|
+
getHiddenPromptIds() {
|
|
19980
|
+
return Array.from(this.hiddenPromptIds);
|
|
19981
|
+
}
|
|
18622
19982
|
/**
|
|
18623
19983
|
* Backwards-compatible alias for `getEffects()`.
|
|
18624
19984
|
*/
|
|
@@ -18718,11 +20078,7 @@ var Session = class {
|
|
|
18718
20078
|
if (!normalizedDocs) {
|
|
18719
20079
|
return normalizedTypes;
|
|
18720
20080
|
}
|
|
18721
|
-
return [
|
|
18722
|
-
normalizedTypes,
|
|
18723
|
-
"Generated usage notes from ./sandbox-tools docs:",
|
|
18724
|
-
normalizedDocs
|
|
18725
|
-
].join("\n\n");
|
|
20081
|
+
return [normalizedTypes, "[Docs]", normalizedDocs].join("\n\n");
|
|
18726
20082
|
}
|
|
18727
20083
|
if (normalizedDocs) {
|
|
18728
20084
|
return normalizedDocs;
|
|
@@ -18944,6 +20300,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
18944
20300
|
const emitPrompt = (payload) => {
|
|
18945
20301
|
const prompt3 = normalizePrompt(payload);
|
|
18946
20302
|
if (!prompt3) return;
|
|
20303
|
+
this.hiddenPromptIds.delete(prompt3.id);
|
|
18947
20304
|
this.promptCache.set(prompt3.id, prompt3);
|
|
18948
20305
|
this.emit("prompt", prompt3);
|
|
18949
20306
|
};
|
|
@@ -19126,6 +20483,7 @@ var JobImplementation = class {
|
|
|
19126
20483
|
eventListeners = /* @__PURE__ */ new Map();
|
|
19127
20484
|
bufferedAgentMessages = [];
|
|
19128
20485
|
bufferedAgentMessageIds = /* @__PURE__ */ new Set();
|
|
20486
|
+
resultSettled = false;
|
|
19129
20487
|
metadata;
|
|
19130
20488
|
constructor(id, client, initialState) {
|
|
19131
20489
|
this.id = id;
|
|
@@ -19150,7 +20508,9 @@ var JobImplementation = class {
|
|
|
19150
20508
|
if (execData.error) {
|
|
19151
20509
|
this.finalize("failed", void 0, execData.error);
|
|
19152
20510
|
} else {
|
|
19153
|
-
this.finalize("succeeded", execData.result
|
|
20511
|
+
this.finalize("succeeded", execData.result, void 0, {
|
|
20512
|
+
hasResult: Object.prototype.hasOwnProperty.call(execData, "result")
|
|
20513
|
+
});
|
|
19154
20514
|
}
|
|
19155
20515
|
this.emit("status", this.status);
|
|
19156
20516
|
}
|
|
@@ -19186,9 +20546,6 @@ var JobImplementation = class {
|
|
|
19186
20546
|
if (normalizedStatus === "failed" || normalizedStatus === "timeout" || normalizedStatus === "canceled") {
|
|
19187
20547
|
this.finalize(normalizedStatus);
|
|
19188
20548
|
}
|
|
19189
|
-
if (normalizedStatus === "succeeded") {
|
|
19190
|
-
this.finalize("succeeded");
|
|
19191
|
-
}
|
|
19192
20549
|
this.emit("status", normalizedStatus);
|
|
19193
20550
|
});
|
|
19194
20551
|
this.client.on(`job.${id}.stdout`, (line) => {
|
|
@@ -19208,7 +20565,7 @@ var JobImplementation = class {
|
|
|
19208
20565
|
this.emit("stderr", line);
|
|
19209
20566
|
});
|
|
19210
20567
|
this.client.on(`job.${id}.result`, (result) => {
|
|
19211
|
-
this.finalize("succeeded", result);
|
|
20568
|
+
this.finalize("succeeded", result, void 0, { hasResult: true });
|
|
19212
20569
|
});
|
|
19213
20570
|
this.client.on(`job.${id}.error`, (error2) => {
|
|
19214
20571
|
this.finalize("failed", void 0, error2);
|
|
@@ -19229,7 +20586,9 @@ var JobImplementation = class {
|
|
|
19229
20586
|
this.client.on("job.completed", (data) => {
|
|
19230
20587
|
const jobData = data;
|
|
19231
20588
|
if (jobData.jobId === id) {
|
|
19232
|
-
this.finalize("succeeded", jobData.result
|
|
20589
|
+
this.finalize("succeeded", jobData.result, void 0, {
|
|
20590
|
+
hasResult: true
|
|
20591
|
+
});
|
|
19233
20592
|
this.emit("status", this.status);
|
|
19234
20593
|
}
|
|
19235
20594
|
});
|
|
@@ -19354,7 +20713,7 @@ var JobImplementation = class {
|
|
|
19354
20713
|
this.metadata.status = "running";
|
|
19355
20714
|
}
|
|
19356
20715
|
}
|
|
19357
|
-
finalize(status, result, error2) {
|
|
20716
|
+
finalize(status, result, error2, options = {}) {
|
|
19358
20717
|
if (!this.metadata.startedAt) {
|
|
19359
20718
|
this.metadata.startedAt = Date.now();
|
|
19360
20719
|
}
|
|
@@ -19362,14 +20721,18 @@ var JobImplementation = class {
|
|
|
19362
20721
|
this.metadata.status = status;
|
|
19363
20722
|
this.metadata.completedAt = this.metadata.completedAt || Date.now();
|
|
19364
20723
|
this.metadata.durationMs = this.metadata.completedAt - this.metadata.startedAt;
|
|
19365
|
-
if (result !== void 0) {
|
|
20724
|
+
if (!this.resultSettled && (options.hasResult || result !== void 0)) {
|
|
19366
20725
|
this.metadata.result = sanitizeFeedbackValue(result);
|
|
20726
|
+
this.resultSettled = true;
|
|
19367
20727
|
this._resolveResult(result);
|
|
19368
20728
|
}
|
|
19369
|
-
if (error2 !== void 0) {
|
|
19370
|
-
const
|
|
20729
|
+
if (!this.resultSettled && (error2 !== void 0 || status === "failed" || status === "timeout" || status === "canceled")) {
|
|
20730
|
+
const fallbackError = new Error(`Job ${this.id} ${status}.`);
|
|
20731
|
+
const cause = error2 ?? fallbackError;
|
|
20732
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
19371
20733
|
this.metadata.error = truncateFeedbackString(message);
|
|
19372
|
-
this.
|
|
20734
|
+
this.resultSettled = true;
|
|
20735
|
+
this._rejectResult(cause);
|
|
19373
20736
|
}
|
|
19374
20737
|
}
|
|
19375
20738
|
upsertToolCall(next) {
|
|
@@ -19452,6 +20815,17 @@ function humanTextFromStdout(stdout) {
|
|
|
19452
20815
|
}
|
|
19453
20816
|
return null;
|
|
19454
20817
|
}
|
|
20818
|
+
function responseTextFromAgentMessages(agentMessages) {
|
|
20819
|
+
for (const message of [...agentMessages].reverse()) {
|
|
20820
|
+
const record = asRecord2(message);
|
|
20821
|
+
if (!record) continue;
|
|
20822
|
+
for (const key of RESPONSE_KEYS) {
|
|
20823
|
+
const normalized = normalizeText(record[key]);
|
|
20824
|
+
if (normalized) return normalized;
|
|
20825
|
+
}
|
|
20826
|
+
}
|
|
20827
|
+
return null;
|
|
20828
|
+
}
|
|
19455
20829
|
function pushString(target, value) {
|
|
19456
20830
|
if (typeof value === "string" && value.trim()) {
|
|
19457
20831
|
target.add(value.trim());
|
|
@@ -19477,6 +20851,41 @@ function collectReferencesFromRecord(record, refs) {
|
|
|
19477
20851
|
for (const key of VARIABLE_ARRAY_KEY_CANDIDATES)
|
|
19478
20852
|
pushStringArray(refs.variableNames, record[key]);
|
|
19479
20853
|
}
|
|
20854
|
+
function stringValue(record, keys) {
|
|
20855
|
+
for (const key of keys) {
|
|
20856
|
+
const value = record[key];
|
|
20857
|
+
if (typeof value === "string" && value.trim()) {
|
|
20858
|
+
return value.trim();
|
|
20859
|
+
}
|
|
20860
|
+
}
|
|
20861
|
+
return null;
|
|
20862
|
+
}
|
|
20863
|
+
function findEntryPathForRecord(record, heap) {
|
|
20864
|
+
const directPath = stringValue(record, ["entryPath", "path"]);
|
|
20865
|
+
if (directPath && heap.entriesByPath?.[directPath]) {
|
|
20866
|
+
return directPath;
|
|
20867
|
+
}
|
|
20868
|
+
const id = stringValue(record, ["id", "_id", "recordId", "objectId"]);
|
|
20869
|
+
if (!id) {
|
|
20870
|
+
return null;
|
|
20871
|
+
}
|
|
20872
|
+
const className = stringValue(record, [
|
|
20873
|
+
"className",
|
|
20874
|
+
"_className",
|
|
20875
|
+
"__className",
|
|
20876
|
+
"prototype",
|
|
20877
|
+
"type"
|
|
20878
|
+
]);
|
|
20879
|
+
const entries = Object.values(heap.entriesByPath || {});
|
|
20880
|
+
const exact = entries.find(
|
|
20881
|
+
(entry) => entry.id === id && (!className || entry.className === className || entry.prototypes?.includes(className))
|
|
20882
|
+
);
|
|
20883
|
+
if (exact?.path) {
|
|
20884
|
+
return exact.path;
|
|
20885
|
+
}
|
|
20886
|
+
const idOnlyMatches = entries.filter((entry) => entry.id === id);
|
|
20887
|
+
return idOnlyMatches.length === 1 ? idOnlyMatches[0].path : null;
|
|
20888
|
+
}
|
|
19480
20889
|
function scanForHeapReferences(value, heap, refs, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
19481
20890
|
if (value === null || value === void 0 || depth > 4 || seen.has(value))
|
|
19482
20891
|
return;
|
|
@@ -19497,6 +20906,8 @@ function scanForHeapReferences(value, heap, refs, depth = 0, seen = /* @__PURE__
|
|
|
19497
20906
|
const record = asRecord2(value);
|
|
19498
20907
|
if (!record) return;
|
|
19499
20908
|
seen.add(value);
|
|
20909
|
+
const entryPath = findEntryPathForRecord(record, heap);
|
|
20910
|
+
if (entryPath) refs.entryPaths.add(entryPath);
|
|
19500
20911
|
collectReferencesFromRecord(record, refs);
|
|
19501
20912
|
for (const key of UI_CONTAINER_KEYS) {
|
|
19502
20913
|
const nested = asRecord2(record[key]);
|
|
@@ -19594,8 +21005,8 @@ function getJobRelatedLists(heap, jobId) {
|
|
|
19594
21005
|
function entriesFromLists(lists, heap) {
|
|
19595
21006
|
const entries = [];
|
|
19596
21007
|
for (const list of lists) {
|
|
19597
|
-
for (const
|
|
19598
|
-
const entry = heap.entriesByPath?.[
|
|
21008
|
+
for (const path7 of list.paths || []) {
|
|
21009
|
+
const entry = heap.entriesByPath?.[path7];
|
|
19599
21010
|
if (entry) entries.push(entry);
|
|
19600
21011
|
}
|
|
19601
21012
|
}
|
|
@@ -19605,6 +21016,7 @@ function resolveJobPresentation({
|
|
|
19605
21016
|
jobId,
|
|
19606
21017
|
result,
|
|
19607
21018
|
stdout = [],
|
|
21019
|
+
agentMessages = [],
|
|
19608
21020
|
sessionHeap,
|
|
19609
21021
|
allowExplicitArtifacts = true
|
|
19610
21022
|
}) {
|
|
@@ -19621,7 +21033,7 @@ function resolveJobPresentation({
|
|
|
19621
21033
|
[...refs.listNames].map((name) => sessionHeap.listsByName?.[name]).filter((list) => Boolean(list))
|
|
19622
21034
|
);
|
|
19623
21035
|
const referencedEntries = sortEntries(
|
|
19624
|
-
[...refs.entryPaths].map((
|
|
21036
|
+
[...refs.entryPaths].map((path7) => sessionHeap.entriesByPath?.[path7]).filter((entry) => Boolean(entry))
|
|
19625
21037
|
);
|
|
19626
21038
|
const jobLists = getJobRelatedLists(sessionHeap, jobId);
|
|
19627
21039
|
const jobEntries = getJobRelatedEntries(sessionHeap, jobId);
|
|
@@ -19637,7 +21049,7 @@ function resolveJobPresentation({
|
|
|
19637
21049
|
const hasExplicitArtifacts = allowExplicitArtifacts && (explicitEntries.length > 0 || explicitLists.length > 0);
|
|
19638
21050
|
const lists = hasExplicitArtifacts ? explicitLists : jobLists;
|
|
19639
21051
|
const entries = hasExplicitArtifacts ? explicitEntries : changedEntries;
|
|
19640
|
-
const responseText = extractResponseText(result, stdout) || fallbackResponseText(entries, lists);
|
|
21052
|
+
const responseText = extractResponseText(result, stdout) || responseTextFromAgentMessages(agentMessages) || fallbackResponseText(entries, lists);
|
|
19641
21053
|
return {
|
|
19642
21054
|
responseText,
|
|
19643
21055
|
entries,
|
|
@@ -20111,6 +21523,110 @@ async function invokeRegisteredEffect(effectMap, request) {
|
|
|
20111
21523
|
return resolved.handler(request.input, context);
|
|
20112
21524
|
}
|
|
20113
21525
|
|
|
21526
|
+
// src/spend.ts
|
|
21527
|
+
function toGranularHttpBase(apiUrl) {
|
|
21528
|
+
const url = new URL(apiUrl);
|
|
21529
|
+
if (url.protocol === "ws:") {
|
|
21530
|
+
url.protocol = "http:";
|
|
21531
|
+
} else if (url.protocol === "wss:") {
|
|
21532
|
+
url.protocol = "https:";
|
|
21533
|
+
}
|
|
21534
|
+
url.pathname = url.pathname.replace(/\/ws\/connect$/, "").replace(/\/ws$/, "");
|
|
21535
|
+
if (!url.pathname || url.pathname === "/") {
|
|
21536
|
+
url.pathname = "/granular";
|
|
21537
|
+
}
|
|
21538
|
+
url.search = "";
|
|
21539
|
+
url.hash = "";
|
|
21540
|
+
return url.toString().replace(/\/$/, "");
|
|
21541
|
+
}
|
|
21542
|
+
function cleanIdPart(value) {
|
|
21543
|
+
return value.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
21544
|
+
}
|
|
21545
|
+
function buildOpenAISpendEventId(usage, context = {}) {
|
|
21546
|
+
const requestId = usage.requestId?.trim();
|
|
21547
|
+
if (!requestId) return void 0;
|
|
21548
|
+
const scope = context.sessionId || context.environmentId || context.subjectId || context.sandboxId || "global";
|
|
21549
|
+
return ["spend", "openai", scope, requestId].map(cleanIdPart).join("_");
|
|
21550
|
+
}
|
|
21551
|
+
function pricingEffectiveAtSeconds(value) {
|
|
21552
|
+
if (!value) return null;
|
|
21553
|
+
const parsed = Date.parse(value);
|
|
21554
|
+
return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : null;
|
|
21555
|
+
}
|
|
21556
|
+
function compactContext(context) {
|
|
21557
|
+
return Object.fromEntries(
|
|
21558
|
+
Object.entries(context).filter(
|
|
21559
|
+
([, value]) => value != null && value !== ""
|
|
21560
|
+
)
|
|
21561
|
+
);
|
|
21562
|
+
}
|
|
21563
|
+
function omitTenantId(context) {
|
|
21564
|
+
const scopedContext = { ...context };
|
|
21565
|
+
delete scopedContext.tenantId;
|
|
21566
|
+
return scopedContext;
|
|
21567
|
+
}
|
|
21568
|
+
async function recordOpenAIUsageSpend(options) {
|
|
21569
|
+
const usageContext = compactContext({
|
|
21570
|
+
...options.usage.usageContext || {},
|
|
21571
|
+
...options.context || {}
|
|
21572
|
+
});
|
|
21573
|
+
const context = omitTenantId(usageContext);
|
|
21574
|
+
const spendEventId = options.usage.spendEventId || buildOpenAISpendEventId(options.usage, context);
|
|
21575
|
+
const metadata = {
|
|
21576
|
+
...options.metadata || {},
|
|
21577
|
+
...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
|
|
21578
|
+
usageContext: context
|
|
21579
|
+
};
|
|
21580
|
+
const response = await fetch(
|
|
21581
|
+
`${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
|
|
21582
|
+
{
|
|
21583
|
+
method: "POST",
|
|
21584
|
+
cache: "no-store",
|
|
21585
|
+
headers: {
|
|
21586
|
+
Authorization: `Bearer ${options.token}`,
|
|
21587
|
+
"Content-Type": "application/json"
|
|
21588
|
+
},
|
|
21589
|
+
body: JSON.stringify({
|
|
21590
|
+
...spendEventId ? { spendEventId } : {},
|
|
21591
|
+
sandboxId: context.sandboxId || null,
|
|
21592
|
+
environmentId: context.environmentId || null,
|
|
21593
|
+
sessionId: context.sessionId || null,
|
|
21594
|
+
subjectId: context.subjectId || null,
|
|
21595
|
+
permissionProfileId: context.permissionProfileId || null,
|
|
21596
|
+
source: "openai",
|
|
21597
|
+
lineItemType: "llm_tokens",
|
|
21598
|
+
provider: options.usage.provider,
|
|
21599
|
+
model: options.usage.model,
|
|
21600
|
+
operation: options.usage.operation || "chat.completions",
|
|
21601
|
+
requestId: options.usage.requestId || null,
|
|
21602
|
+
inputTokens: options.usage.inputTokens,
|
|
21603
|
+
outputTokens: options.usage.outputTokens,
|
|
21604
|
+
cachedInputTokens: options.usage.cachedInputTokens,
|
|
21605
|
+
reasoningTokens: options.usage.reasoningTokens,
|
|
21606
|
+
quantity: options.usage.totalTokens,
|
|
21607
|
+
quantityUnit: "tokens",
|
|
21608
|
+
inputPricePerMillionMicros: options.usage.inputPricePerMillionMicros,
|
|
21609
|
+
cachedInputPricePerMillionMicros: options.usage.cachedInputPricePerMillionMicros,
|
|
21610
|
+
outputPricePerMillionMicros: options.usage.outputPricePerMillionMicros,
|
|
21611
|
+
amountMicros: options.usage.amountMicros,
|
|
21612
|
+
currency: options.usage.currency,
|
|
21613
|
+
pricingSource: options.usage.pricingSource,
|
|
21614
|
+
pricingEffectiveAt: pricingEffectiveAtSeconds(
|
|
21615
|
+
options.usage.pricingEffectiveAt
|
|
21616
|
+
),
|
|
21617
|
+
estimated: false,
|
|
21618
|
+
metadata
|
|
21619
|
+
})
|
|
21620
|
+
}
|
|
21621
|
+
);
|
|
21622
|
+
if (!response.ok) {
|
|
21623
|
+
throw new Error(
|
|
21624
|
+
`Granular spend event failed (${response.status}): ${await response.text()}`
|
|
21625
|
+
);
|
|
21626
|
+
}
|
|
21627
|
+
return response.json();
|
|
21628
|
+
}
|
|
21629
|
+
|
|
20114
21630
|
// src/manifest-metamodels.ts
|
|
20115
21631
|
function buildFieldMetamodelMutations(fieldPath, spec) {
|
|
20116
21632
|
return DEFAULT_METAMODEL_PACKAGES.flatMap(
|
|
@@ -20172,6 +21688,12 @@ var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT = 3;
|
|
|
20172
21688
|
var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_DELAY_MS = 1e3;
|
|
20173
21689
|
var LOCAL_CONTROL_REQUEST_RETRY_COUNT = 4;
|
|
20174
21690
|
var LOCAL_CONTROL_REQUEST_RETRY_DELAY_MS = 500;
|
|
21691
|
+
var SESSION_DATA_REQUEST_RETRY_COUNT = 4;
|
|
21692
|
+
var SESSION_DATA_REQUEST_RETRY_DELAY_MS = 500;
|
|
21693
|
+
var EFFECT_HOST_CONNECT_TIMEOUT_MS = 15e3;
|
|
21694
|
+
var EFFECT_CATALOG_SYNC_TIMEOUT_MS = 3e4;
|
|
21695
|
+
var EFFECT_CATALOG_SYNC_RETRY_COUNT = 3;
|
|
21696
|
+
var EFFECT_CATALOG_SYNC_RETRY_DELAY_MS = 1e3;
|
|
20175
21697
|
function planRecordObjectsChunks(records, batchSize) {
|
|
20176
21698
|
const total = records.length;
|
|
20177
21699
|
const size = Math.max(1, Math.min(batchSize, total));
|
|
@@ -20186,6 +21708,19 @@ function planRecordObjectsChunks(records, batchSize) {
|
|
|
20186
21708
|
function sleep(ms) {
|
|
20187
21709
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
20188
21710
|
}
|
|
21711
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
21712
|
+
let timer = null;
|
|
21713
|
+
const timeout = new Promise((_, reject) => {
|
|
21714
|
+
timer = setTimeout(() => {
|
|
21715
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
21716
|
+
}, timeoutMs);
|
|
21717
|
+
});
|
|
21718
|
+
return Promise.race([promise, timeout]).finally(() => {
|
|
21719
|
+
if (timer) {
|
|
21720
|
+
clearTimeout(timer);
|
|
21721
|
+
}
|
|
21722
|
+
});
|
|
21723
|
+
}
|
|
20189
21724
|
function isLocalControlUrl(url) {
|
|
20190
21725
|
try {
|
|
20191
21726
|
const parsed = new URL(url);
|
|
@@ -20199,7 +21734,19 @@ function isRetryableLocalWorkerRestart(status, body, url) {
|
|
|
20199
21734
|
}
|
|
20200
21735
|
function isRetryableRecordObjectsError(error2) {
|
|
20201
21736
|
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
20202
|
-
return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out/i.test(
|
|
21737
|
+
return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out|bad gateway|too many requests|gateway timeout|control plane api error \((?:429|500|502|503|504)\)|graphql api error \((?:429|500|502|503|504)\)|failed to record batch/i.test(
|
|
21738
|
+
message
|
|
21739
|
+
);
|
|
21740
|
+
}
|
|
21741
|
+
function isRetryableEffectRegistrationError(error2) {
|
|
21742
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
21743
|
+
return /timed out|websocket disconnected|websocket not connected|rpc timeout|worker restarted mid-request|network connection lost|bad gateway|gateway timeout|too many requests|(?:control plane|granular|graphql) api error \((?:429|500|502|503|504)\)/i.test(
|
|
21744
|
+
message
|
|
21745
|
+
);
|
|
21746
|
+
}
|
|
21747
|
+
function isRetryableSessionDataError(error2) {
|
|
21748
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
21749
|
+
return /network connection lost|worker restarted mid-request|econnreset|socket connection was closed unexpectedly|bad gateway|gateway timeout|service unavailable|session data api error \((?:429|500|502|503|504)\)/i.test(
|
|
20203
21750
|
message
|
|
20204
21751
|
);
|
|
20205
21752
|
}
|
|
@@ -20221,16 +21768,28 @@ function computeEffectRegistrationKey(effect) {
|
|
|
20221
21768
|
effect.versionSelector
|
|
20222
21769
|
)}`;
|
|
20223
21770
|
}
|
|
20224
|
-
function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId) {
|
|
20225
|
-
const
|
|
20226
|
-
|
|
21771
|
+
function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId, effectHostUrl) {
|
|
21772
|
+
const overrideUrl = effectHostUrl || process.env.GRANULAR_EFFECT_HOST_URL || process.env.EFFECT_HOST_URL;
|
|
21773
|
+
const api = new URL(apiUrl);
|
|
21774
|
+
const localRuntimeBase = process.env.RUNTIME_ORCHESTRATOR_URL || (isLocalControlUrl(apiUrl) ? `${api.protocol}//${api.hostname}:8791` : "");
|
|
21775
|
+
const url = new URL(overrideUrl || localRuntimeBase || apiUrl);
|
|
21776
|
+
if (url.protocol === "https:") {
|
|
21777
|
+
url.protocol = "wss:";
|
|
21778
|
+
} else if (url.protocol === "http:") {
|
|
21779
|
+
url.protocol = "ws:";
|
|
21780
|
+
}
|
|
21781
|
+
if (!overrideUrl && isLocalControlUrl(apiUrl) && api.pathname.endsWith("/granular")) {
|
|
21782
|
+
url.pathname = "/granular/orchestrator/effects/connect";
|
|
21783
|
+
} else if (url.pathname.endsWith("/granular/ws/connect")) {
|
|
20227
21784
|
url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
|
|
20228
21785
|
} else if (url.pathname.endsWith("/granular")) {
|
|
20229
|
-
url.pathname = `${url.pathname.replace(/\/$/, "")}/effects/connect`;
|
|
21786
|
+
url.pathname = isLocalControlUrl(url.toString()) ? "/granular/orchestrator/effects/connect" : `${url.pathname.replace(/\/$/, "")}/effects/connect`;
|
|
20230
21787
|
} else if (url.pathname.endsWith("/v2/ws/connect")) {
|
|
20231
21788
|
url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
|
|
20232
21789
|
} else if (url.pathname.endsWith("/v2/ws")) {
|
|
20233
21790
|
url.pathname = url.pathname.replace(/\/ws$/, "/effects/connect");
|
|
21791
|
+
} else if (url.pathname === "/" && isLocalControlUrl(url.toString()) && (url.port === "8791" || !overrideUrl && Boolean(localRuntimeBase))) {
|
|
21792
|
+
url.pathname = "/granular/orchestrator/effects/connect";
|
|
20234
21793
|
} else if (url.pathname.endsWith("/ws/connect")) {
|
|
20235
21794
|
url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
|
|
20236
21795
|
} else if (url.pathname.endsWith("/ws")) {
|
|
@@ -20265,6 +21824,79 @@ function normalizeHeapSnapshot(raw) {
|
|
|
20265
21824
|
updatedAt: typeof heap.updatedAt === "number" ? heap.updatedAt : Date.now()
|
|
20266
21825
|
};
|
|
20267
21826
|
}
|
|
21827
|
+
function normalizeGraphPathSegment(value) {
|
|
21828
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
21829
|
+
}
|
|
21830
|
+
function extractRecordIdFromGraphPath(path7, className) {
|
|
21831
|
+
const normalizedPrefix = `${normalizeGraphPathSegment(className)}_`;
|
|
21832
|
+
if (path7.startsWith(normalizedPrefix)) {
|
|
21833
|
+
return path7.slice(normalizedPrefix.length);
|
|
21834
|
+
}
|
|
21835
|
+
const legacyPrefix = `${className}_`;
|
|
21836
|
+
if (path7.startsWith(legacyPrefix)) {
|
|
21837
|
+
return path7.slice(legacyPrefix.length);
|
|
21838
|
+
}
|
|
21839
|
+
return path7;
|
|
21840
|
+
}
|
|
21841
|
+
function toRecordSearchResult(className, node) {
|
|
21842
|
+
const path7 = typeof node.path === "string" ? node.path : "";
|
|
21843
|
+
if (!path7) return null;
|
|
21844
|
+
const fields = Array.isArray(node.submodels) ? node.submodels.flatMap(
|
|
21845
|
+
(submodel) => {
|
|
21846
|
+
const name = typeof submodel?.label === "string" && submodel.label.trim() ? submodel.label : typeof submodel?.path === "string" ? submodel.path.split(":").pop() || submodel.path : "";
|
|
21847
|
+
if (!name) return [];
|
|
21848
|
+
if (typeof submodel.string_value === "string") {
|
|
21849
|
+
return [{ name, type: "string", value: submodel.string_value }];
|
|
21850
|
+
}
|
|
21851
|
+
if (typeof submodel.number_value === "number") {
|
|
21852
|
+
return [{ name, type: "number", value: submodel.number_value }];
|
|
21853
|
+
}
|
|
21854
|
+
if (typeof submodel.boolean_value === "boolean") {
|
|
21855
|
+
return [
|
|
21856
|
+
{
|
|
21857
|
+
name,
|
|
21858
|
+
type: "boolean",
|
|
21859
|
+
value: submodel.boolean_value
|
|
21860
|
+
}
|
|
21861
|
+
];
|
|
21862
|
+
}
|
|
21863
|
+
return [];
|
|
21864
|
+
}
|
|
21865
|
+
) : [];
|
|
21866
|
+
return {
|
|
21867
|
+
path: path7,
|
|
21868
|
+
className,
|
|
21869
|
+
id: extractRecordIdFromGraphPath(path7, className),
|
|
21870
|
+
label: typeof node.label === "string" && node.label.trim() ? node.label : extractRecordIdFromGraphPath(path7, className),
|
|
21871
|
+
description: typeof node.description === "string" && node.description.trim() ? node.description : null,
|
|
21872
|
+
fields
|
|
21873
|
+
};
|
|
21874
|
+
}
|
|
21875
|
+
function normalizeRecordSearchText(value) {
|
|
21876
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
|
|
21877
|
+
}
|
|
21878
|
+
function rankRecordSearchResult(result, query, index) {
|
|
21879
|
+
const normalizedQuery = normalizeRecordSearchText(query);
|
|
21880
|
+
if (!normalizedQuery) {
|
|
21881
|
+
return index;
|
|
21882
|
+
}
|
|
21883
|
+
const label = normalizeRecordSearchText(result.label || "");
|
|
21884
|
+
const id = normalizeRecordSearchText(result.id || "");
|
|
21885
|
+
const path7 = normalizeRecordSearchText(result.path || "");
|
|
21886
|
+
const className = normalizeRecordSearchText(result.className || "");
|
|
21887
|
+
const searchable = [label, id, path7, className].filter(Boolean);
|
|
21888
|
+
if (label === normalizedQuery) return index;
|
|
21889
|
+
if (id === normalizedQuery || path7 === normalizedQuery) return 100 + index;
|
|
21890
|
+
if (label.startsWith(normalizedQuery)) return 200 + index;
|
|
21891
|
+
if (searchable.some((value) => value.startsWith(normalizedQuery))) {
|
|
21892
|
+
return 300 + index;
|
|
21893
|
+
}
|
|
21894
|
+
if (label.includes(normalizedQuery)) return 400 + index;
|
|
21895
|
+
if (searchable.some((value) => value.includes(normalizedQuery))) {
|
|
21896
|
+
return 500 + index;
|
|
21897
|
+
}
|
|
21898
|
+
return 900 + index;
|
|
21899
|
+
}
|
|
20268
21900
|
function deriveRuntimeBaseUrl(apiEndpoint) {
|
|
20269
21901
|
try {
|
|
20270
21902
|
const endpoint = new URL(apiEndpoint);
|
|
@@ -20353,7 +21985,7 @@ function normalizeEnvironmentData(environment) {
|
|
|
20353
21985
|
setup: normalizeEnvironmentSetupSummary(environment.setup)
|
|
20354
21986
|
};
|
|
20355
21987
|
}
|
|
20356
|
-
var Environment = class {
|
|
21988
|
+
var Environment = class _Environment {
|
|
20357
21989
|
granular;
|
|
20358
21990
|
envData;
|
|
20359
21991
|
_apiKey;
|
|
@@ -20528,9 +22160,9 @@ var Environment = class {
|
|
|
20528
22160
|
getRuntimeBaseUrl() {
|
|
20529
22161
|
return deriveRuntimeBaseUrl(this._apiEndpoint);
|
|
20530
22162
|
}
|
|
20531
|
-
async controlPlaneRequest(
|
|
22163
|
+
async controlPlaneRequest(path7, options = {}) {
|
|
20532
22164
|
const runtimeBase = this.getRuntimeBaseUrl();
|
|
20533
|
-
const url = `${runtimeBase}${
|
|
22165
|
+
const url = `${runtimeBase}${path7}`;
|
|
20534
22166
|
const response = await fetch(url, {
|
|
20535
22167
|
...options,
|
|
20536
22168
|
headers: {
|
|
@@ -20548,28 +22180,30 @@ var Environment = class {
|
|
|
20548
22180
|
return response.json();
|
|
20549
22181
|
}
|
|
20550
22182
|
// ==================== ID ↔ GRAPH PATH MAPPING ====================
|
|
22183
|
+
static normalizeGraphPathSegment(value) {
|
|
22184
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
22185
|
+
}
|
|
20551
22186
|
/**
|
|
20552
|
-
* Convert a class name +
|
|
20553
|
-
*
|
|
20554
|
-
* Two objects of *different* classes may share the same real-world ID,
|
|
20555
|
-
* so the graph path must incorporate the class to guarantee uniqueness.
|
|
22187
|
+
* Convert a class name + application record ID into Granular's graph path.
|
|
20556
22188
|
*
|
|
20557
|
-
*
|
|
20558
|
-
*
|
|
20559
|
-
*
|
|
20560
|
-
* underscores (e.g. `author`, `book`). This ensures the prefix is
|
|
20561
|
-
* unambiguously parseable by `extractIdFromGraphPath`.
|
|
22189
|
+
* This mirrors the record-write path normalization used by the control plane.
|
|
22190
|
+
* Keep the original customer/system ID in `real_id`; graph paths are stable
|
|
22191
|
+
* internal addresses, not the source of truth for business identity.
|
|
20562
22192
|
*/
|
|
20563
22193
|
static toGraphPath(className, id) {
|
|
20564
|
-
return `${className}_${id}`;
|
|
22194
|
+
return `${_Environment.normalizeGraphPathSegment(className)}_${_Environment.normalizeGraphPathSegment(id)}`;
|
|
20565
22195
|
}
|
|
20566
22196
|
/**
|
|
20567
|
-
*
|
|
22197
|
+
* Best-effort extraction of an ID-like suffix from a graph path.
|
|
20568
22198
|
*
|
|
20569
|
-
*
|
|
20570
|
-
*
|
|
22199
|
+
* Prefer the record's `real_id` field whenever exact customer/system IDs
|
|
22200
|
+
* matter, because graph path normalization is intentionally lossy.
|
|
20571
22201
|
*/
|
|
20572
22202
|
static extractIdFromGraphPath(graphPath, className) {
|
|
22203
|
+
const normalizedPrefix = `${_Environment.normalizeGraphPathSegment(className)}_`;
|
|
22204
|
+
if (graphPath.startsWith(normalizedPrefix)) {
|
|
22205
|
+
return graphPath.substring(normalizedPrefix.length);
|
|
22206
|
+
}
|
|
20573
22207
|
const prefix = `${className}_`;
|
|
20574
22208
|
return graphPath.startsWith(prefix) ? graphPath.substring(prefix.length) : graphPath;
|
|
20575
22209
|
}
|
|
@@ -20616,6 +22250,62 @@ var Environment = class {
|
|
|
20616
22250
|
}
|
|
20617
22251
|
return response.json();
|
|
20618
22252
|
}
|
|
22253
|
+
async searchRecords(query, options = {}) {
|
|
22254
|
+
const normalizedQuery = query.replace(/\s+/g, " ").trim();
|
|
22255
|
+
const limit = Math.max(1, Math.min(50, Math.floor(options.limit ?? 12)));
|
|
22256
|
+
const offset = Math.max(0, Math.floor(options.offset ?? 0));
|
|
22257
|
+
const response = await this.graphql(
|
|
22258
|
+
`
|
|
22259
|
+
query RecordMentionSearch(
|
|
22260
|
+
$query: String
|
|
22261
|
+
$limit: Int
|
|
22262
|
+
$offset: Int
|
|
22263
|
+
$classNames: [String!]
|
|
22264
|
+
) {
|
|
22265
|
+
record_search(
|
|
22266
|
+
query: $query
|
|
22267
|
+
limit: $limit
|
|
22268
|
+
offset: $offset
|
|
22269
|
+
class_names: $classNames
|
|
22270
|
+
) {
|
|
22271
|
+
className
|
|
22272
|
+
model {
|
|
22273
|
+
path
|
|
22274
|
+
label
|
|
22275
|
+
description
|
|
22276
|
+
submodels {
|
|
22277
|
+
path
|
|
22278
|
+
label
|
|
22279
|
+
string_value
|
|
22280
|
+
number_value
|
|
22281
|
+
boolean_value
|
|
22282
|
+
}
|
|
22283
|
+
}
|
|
22284
|
+
}
|
|
22285
|
+
}
|
|
22286
|
+
`,
|
|
22287
|
+
{
|
|
22288
|
+
query: normalizedQuery,
|
|
22289
|
+
limit,
|
|
22290
|
+
offset,
|
|
22291
|
+
classNames: options.classNames?.length ? options.classNames : []
|
|
22292
|
+
}
|
|
22293
|
+
);
|
|
22294
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22295
|
+
const results = (response.data?.record_search || []).flatMap((entry) => {
|
|
22296
|
+
const className = entry.className?.trim();
|
|
22297
|
+
const item = className && entry.model ? toRecordSearchResult(className, entry.model) : null;
|
|
22298
|
+
if (!item || seen.has(item.path)) {
|
|
22299
|
+
return [];
|
|
22300
|
+
}
|
|
22301
|
+
seen.add(item.path);
|
|
22302
|
+
return [item];
|
|
22303
|
+
});
|
|
22304
|
+
return results.map((result, index) => ({
|
|
22305
|
+
result,
|
|
22306
|
+
rank: rankRecordSearchResult(result, normalizedQuery, index)
|
|
22307
|
+
})).sort((left, right) => left.rank - right.rank).map((item) => item.result).slice(0, limit);
|
|
22308
|
+
}
|
|
20619
22309
|
// ==================== RELATIONSHIP METHODS ====================
|
|
20620
22310
|
/**
|
|
20621
22311
|
* Define a relationship between two model types.
|
|
@@ -21381,7 +23071,8 @@ var Environment = class {
|
|
|
21381
23071
|
body: JSON.stringify({
|
|
21382
23072
|
records,
|
|
21383
23073
|
batchSize: options.batchSize,
|
|
21384
|
-
setupRunId: options.setupRunId
|
|
23074
|
+
setupRunId: options.setupRunId,
|
|
23075
|
+
writeMode: options.writeMode
|
|
21385
23076
|
})
|
|
21386
23077
|
}
|
|
21387
23078
|
);
|
|
@@ -21433,11 +23124,13 @@ var Environment = class {
|
|
|
21433
23124
|
};
|
|
21434
23125
|
var EnvironmentSession = class extends Session {
|
|
21435
23126
|
environment;
|
|
23127
|
+
sessionDataRoutePrefix;
|
|
21436
23128
|
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
21437
23129
|
graphContainerStatus = null;
|
|
21438
|
-
constructor(client, environment, clientId) {
|
|
21439
|
-
super(client, clientId);
|
|
23130
|
+
constructor(client, environment, clientId, options = {}) {
|
|
23131
|
+
super(client, clientId, { initialQuota: options.initialQuota });
|
|
21440
23132
|
this.environment = environment;
|
|
23133
|
+
this.sessionDataRoutePrefix = options.sessionDataRoutePrefix || "/orchestrator/ws/sessions";
|
|
21441
23134
|
}
|
|
21442
23135
|
get environmentId() {
|
|
21443
23136
|
return this.environment.environmentId;
|
|
@@ -21482,7 +23175,7 @@ var EnvironmentSession = class extends Session {
|
|
|
21482
23175
|
const doc = this.document;
|
|
21483
23176
|
return normalizeHeapSnapshot(doc?.heap);
|
|
21484
23177
|
}
|
|
21485
|
-
async sessionDataRequest(
|
|
23178
|
+
async sessionDataRequest(path7, query, init2 = {}) {
|
|
21486
23179
|
const searchParams = new URLSearchParams();
|
|
21487
23180
|
for (const [key, value] of Object.entries(query || {})) {
|
|
21488
23181
|
if (value !== null && typeof value !== "undefined" && value !== "") {
|
|
@@ -21490,23 +23183,39 @@ var EnvironmentSession = class extends Session {
|
|
|
21490
23183
|
}
|
|
21491
23184
|
}
|
|
21492
23185
|
const queryString = searchParams.toString();
|
|
21493
|
-
const
|
|
21494
|
-
|
|
21495
|
-
|
|
21496
|
-
|
|
21497
|
-
|
|
21498
|
-
|
|
21499
|
-
|
|
23186
|
+
const url = `${this.environment.runtimeBaseUrl}${this.sessionDataRoutePrefix}/${encodeURIComponent(this.sessionId)}${path7}${queryString ? `?${queryString}` : ""}`;
|
|
23187
|
+
const body = typeof init2.body === "undefined" ? void 0 : JSON.stringify(init2.body);
|
|
23188
|
+
for (let attempt = 1; attempt <= SESSION_DATA_REQUEST_RETRY_COUNT; attempt += 1) {
|
|
23189
|
+
try {
|
|
23190
|
+
const response = await fetch(url, {
|
|
23191
|
+
method: init2.method || "GET",
|
|
23192
|
+
headers: {
|
|
23193
|
+
Authorization: `Bearer ${this.environment.authToken}`,
|
|
23194
|
+
"Content-Type": "application/json"
|
|
23195
|
+
},
|
|
23196
|
+
...typeof body === "undefined" ? {} : { body }
|
|
23197
|
+
});
|
|
23198
|
+
if (response.ok) {
|
|
23199
|
+
return response.json();
|
|
23200
|
+
}
|
|
23201
|
+
const errorText = await response.text();
|
|
23202
|
+
const error2 = new Error(
|
|
23203
|
+
`Session data API Error (${response.status}): ${errorText}`
|
|
23204
|
+
);
|
|
23205
|
+
if (isLocalControlUrl(url) && isRetryableSessionDataError(error2) && attempt < SESSION_DATA_REQUEST_RETRY_COUNT) {
|
|
23206
|
+
await sleep(SESSION_DATA_REQUEST_RETRY_DELAY_MS * attempt);
|
|
23207
|
+
continue;
|
|
21500
23208
|
}
|
|
23209
|
+
throw error2;
|
|
23210
|
+
} catch (error2) {
|
|
23211
|
+
if (isLocalControlUrl(url) && isRetryableSessionDataError(error2) && attempt < SESSION_DATA_REQUEST_RETRY_COUNT) {
|
|
23212
|
+
await sleep(SESSION_DATA_REQUEST_RETRY_DELAY_MS * attempt);
|
|
23213
|
+
continue;
|
|
23214
|
+
}
|
|
23215
|
+
throw error2;
|
|
21501
23216
|
}
|
|
21502
|
-
);
|
|
21503
|
-
if (!response.ok) {
|
|
21504
|
-
const errorText = await response.text();
|
|
21505
|
-
throw new Error(
|
|
21506
|
-
`Session data API Error (${response.status}): ${errorText}`
|
|
21507
|
-
);
|
|
21508
23217
|
}
|
|
21509
|
-
|
|
23218
|
+
throw new Error(`Session data API Error: exhausted retries for ${url}`);
|
|
21510
23219
|
}
|
|
21511
23220
|
async collectAllSessionItems(listPage) {
|
|
21512
23221
|
const items = [];
|
|
@@ -21552,8 +23261,8 @@ var EnvironmentSession = class extends Session {
|
|
|
21552
23261
|
return {
|
|
21553
23262
|
entries: {
|
|
21554
23263
|
list: (options = {}) => this.sessionDataRequest("/heap/entries", options),
|
|
21555
|
-
get: (
|
|
21556
|
-
`/heap/entries/${encodeURIComponent(
|
|
23264
|
+
get: (path7) => this.sessionDataRequest(
|
|
23265
|
+
`/heap/entries/${encodeURIComponent(path7)}`
|
|
21557
23266
|
)
|
|
21558
23267
|
},
|
|
21559
23268
|
lists: {
|
|
@@ -21564,6 +23273,17 @@ var EnvironmentSession = class extends Session {
|
|
|
21564
23273
|
get: (name) => this.sessionDataRequest(
|
|
21565
23274
|
`/heap/lists/${encodeURIComponent(name)}`
|
|
21566
23275
|
)
|
|
23276
|
+
},
|
|
23277
|
+
variables: {
|
|
23278
|
+
list: (options = {}) => this.sessionDataRequest("/heap/variables", options),
|
|
23279
|
+
get: (name) => this.sessionDataRequest(
|
|
23280
|
+
`/heap/variables/${encodeURIComponent(name)}`
|
|
23281
|
+
),
|
|
23282
|
+
delete: (name) => this.sessionDataRequest(
|
|
23283
|
+
`/heap/variables/${encodeURIComponent(name)}`,
|
|
23284
|
+
void 0,
|
|
23285
|
+
{ method: "DELETE" }
|
|
23286
|
+
)
|
|
21567
23287
|
}
|
|
21568
23288
|
};
|
|
21569
23289
|
}
|
|
@@ -21632,6 +23352,19 @@ var EnvironmentSession = class extends Session {
|
|
|
21632
23352
|
async graphql(query, variables) {
|
|
21633
23353
|
return this.environment.graphql(query, variables);
|
|
21634
23354
|
}
|
|
23355
|
+
async searchRecords(query, options = {}) {
|
|
23356
|
+
return this.environment.searchRecords(query, options);
|
|
23357
|
+
}
|
|
23358
|
+
async mentionRecord(input) {
|
|
23359
|
+
return this.sessionDataRequest(
|
|
23360
|
+
"/records/mention",
|
|
23361
|
+
void 0,
|
|
23362
|
+
{
|
|
23363
|
+
method: "POST",
|
|
23364
|
+
body: input
|
|
23365
|
+
}
|
|
23366
|
+
);
|
|
23367
|
+
}
|
|
21635
23368
|
async defineRelationship(options) {
|
|
21636
23369
|
return this.environment.defineRelationship(options);
|
|
21637
23370
|
}
|
|
@@ -21779,6 +23512,7 @@ var Granular = class _Granular {
|
|
|
21779
23512
|
WebSocketCtor;
|
|
21780
23513
|
onUnexpectedClose;
|
|
21781
23514
|
onReconnectError;
|
|
23515
|
+
effectHostUrl;
|
|
21782
23516
|
debugHttp = process.env.GRANULAR_DEBUG_HTTP === "1";
|
|
21783
23517
|
/** Sandbox-level effect registry: sandboxId → (effectKey@selector → ToolWithHandler) */
|
|
21784
23518
|
sandboxEffects = /* @__PURE__ */ new Map();
|
|
@@ -21807,6 +23541,7 @@ var Granular = class _Granular {
|
|
|
21807
23541
|
this.WebSocketCtor = options.WebSocketCtor;
|
|
21808
23542
|
this.onUnexpectedClose = options.onUnexpectedClose;
|
|
21809
23543
|
this.onReconnectError = options.onReconnectError;
|
|
23544
|
+
this.effectHostUrl = options.effectHostUrl;
|
|
21810
23545
|
this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
|
|
21811
23546
|
}
|
|
21812
23547
|
/**
|
|
@@ -21977,6 +23712,30 @@ var Granular = class _Granular {
|
|
|
21977
23712
|
permissions: options.permissions || options.user?.permissions || []
|
|
21978
23713
|
});
|
|
21979
23714
|
}
|
|
23715
|
+
/**
|
|
23716
|
+
* Run a registered environment importer against an environment that was
|
|
23717
|
+
* opened outside this SDK instance, for example by a delegated browser flow.
|
|
23718
|
+
*
|
|
23719
|
+
* This uses the same setup-run and queued record-import plumbing as
|
|
23720
|
+
* `openEnvironment()`: importer stages, expected object counts, and queued
|
|
23721
|
+
* import counters remain visible through `environment.setup` and
|
|
23722
|
+
* `getRecordImportSummary()`.
|
|
23723
|
+
*/
|
|
23724
|
+
async runEnvironmentImporterForEnvironment(environmentId, options = {}) {
|
|
23725
|
+
const environmentData = await this.environments.get(environmentId);
|
|
23726
|
+
const environment = this.bindEnvironmentHandle(environmentData);
|
|
23727
|
+
const requestedOntology = options.ontology || environmentData.ontologyId || environmentData.sandboxId;
|
|
23728
|
+
return this.runEnvironmentImporter(
|
|
23729
|
+
{
|
|
23730
|
+
environment: environmentData,
|
|
23731
|
+
requestedOntology,
|
|
23732
|
+
sandboxId: environmentData.sandboxId,
|
|
23733
|
+
subjectId: environmentData.subjectId,
|
|
23734
|
+
setupTriggerReason: options.reason || "new_environment"
|
|
23735
|
+
},
|
|
23736
|
+
environment
|
|
23737
|
+
);
|
|
23738
|
+
}
|
|
21980
23739
|
resolveRequestedTag(options, methodName) {
|
|
21981
23740
|
const tag2 = options.tag?.trim() || options.tagName?.trim() || options.environment?.trim();
|
|
21982
23741
|
if (!tag2) {
|
|
@@ -22168,6 +23927,15 @@ var Granular = class _Granular {
|
|
|
22168
23927
|
const environment = this.bindEnvironmentHandle(envData);
|
|
22169
23928
|
return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
|
|
22170
23929
|
}
|
|
23930
|
+
async recordOpenAIUsageSpend(usage, context, options) {
|
|
23931
|
+
return recordOpenAIUsageSpend({
|
|
23932
|
+
apiUrl: this.apiUrl,
|
|
23933
|
+
token: this.apiKey,
|
|
23934
|
+
usage,
|
|
23935
|
+
context,
|
|
23936
|
+
metadata: options?.metadata
|
|
23937
|
+
});
|
|
23938
|
+
}
|
|
22171
23939
|
/**
|
|
22172
23940
|
* Mark a session closed in the control plane. If `environment` is the connected handle for that
|
|
22173
23941
|
* `sessionId`, disconnects the WebSocket so the runtime tears down cleanly.
|
|
@@ -22220,15 +23988,25 @@ var Granular = class _Granular {
|
|
|
22220
23988
|
return ontologyImporter;
|
|
22221
23989
|
}
|
|
22222
23990
|
async maybeRunEnvironmentImporter(resolved, environment) {
|
|
22223
|
-
|
|
22224
|
-
|
|
23991
|
+
const setupTriggerReason = resolved.setupTriggerReason;
|
|
23992
|
+
if (!setupTriggerReason) {
|
|
23993
|
+
return null;
|
|
22225
23994
|
}
|
|
23995
|
+
return this.runEnvironmentImporter(
|
|
23996
|
+
{
|
|
23997
|
+
...resolved,
|
|
23998
|
+
setupTriggerReason
|
|
23999
|
+
},
|
|
24000
|
+
environment
|
|
24001
|
+
);
|
|
24002
|
+
}
|
|
24003
|
+
async runEnvironmentImporter(resolved, environment) {
|
|
22226
24004
|
const importer = this.resolveEnvironmentImporter(
|
|
22227
24005
|
resolved.requestedOntology,
|
|
22228
24006
|
resolved.sandboxId
|
|
22229
24007
|
);
|
|
22230
24008
|
if (!importer) {
|
|
22231
|
-
return;
|
|
24009
|
+
return null;
|
|
22232
24010
|
}
|
|
22233
24011
|
const setupRun = await this.request(
|
|
22234
24012
|
`/control/environments/${environment.environmentId}/setup-runs`,
|
|
@@ -22268,16 +24046,24 @@ var Granular = class _Granular {
|
|
|
22268
24046
|
},
|
|
22269
24047
|
importRecords: async (records, options) => environment.enqueueRecordImport(records, {
|
|
22270
24048
|
batchSize: options?.batchSize,
|
|
24049
|
+
writeMode: options?.writeMode,
|
|
22271
24050
|
setupRunId
|
|
22272
24051
|
})
|
|
22273
24052
|
};
|
|
22274
24053
|
try {
|
|
22275
24054
|
await importer(importerContext);
|
|
22276
|
-
|
|
24055
|
+
const completedSetupRun = await this.request(
|
|
24056
|
+
`/control/environment-setup-runs/${setupRunId}`,
|
|
24057
|
+
{
|
|
24058
|
+
method: "PATCH",
|
|
24059
|
+
body: JSON.stringify({ markHookCompleted: true })
|
|
24060
|
+
}
|
|
24061
|
+
);
|
|
22277
24062
|
const refreshedEnvironment = await this.environments.get(
|
|
22278
24063
|
environment.environmentId
|
|
22279
24064
|
);
|
|
22280
24065
|
environment.syncEnvironmentData(refreshedEnvironment);
|
|
24066
|
+
return completedSetupRun;
|
|
22281
24067
|
} catch (error2) {
|
|
22282
24068
|
await updateSetupRun({
|
|
22283
24069
|
status: "failed",
|
|
@@ -22304,7 +24090,8 @@ var Granular = class _Granular {
|
|
|
22304
24090
|
const environmentSession = new EnvironmentSession(
|
|
22305
24091
|
client,
|
|
22306
24092
|
environment,
|
|
22307
|
-
clientId
|
|
24093
|
+
clientId,
|
|
24094
|
+
{ initialQuota: session2.quota || null }
|
|
22308
24095
|
);
|
|
22309
24096
|
await environmentSession.hello();
|
|
22310
24097
|
return environmentSession;
|
|
@@ -22325,27 +24112,45 @@ var Granular = class _Granular {
|
|
|
22325
24112
|
return effects2;
|
|
22326
24113
|
}
|
|
22327
24114
|
serializeEffect(effect) {
|
|
22328
|
-
|
|
24115
|
+
const serialized = {
|
|
22329
24116
|
effectKey: computeEffectKey2(effect),
|
|
22330
24117
|
name: effect.name,
|
|
22331
24118
|
description: effect.description,
|
|
22332
24119
|
inputSchema: effect.inputSchema,
|
|
22333
|
-
outputSchema: effect.outputSchema,
|
|
22334
24120
|
stability: effect.stability || "stable",
|
|
22335
|
-
provenance: effect.provenance || { source: "custom" }
|
|
22336
|
-
tags: effect.tags,
|
|
22337
|
-
className: effect.className,
|
|
22338
|
-
static: effect.static,
|
|
22339
|
-
versionSelector: effect.versionSelector
|
|
24121
|
+
provenance: effect.provenance || { source: "custom" }
|
|
22340
24122
|
};
|
|
24123
|
+
if (effect.outputSchema !== void 0) {
|
|
24124
|
+
serialized.outputSchema = effect.outputSchema;
|
|
24125
|
+
}
|
|
24126
|
+
if (effect.tags !== void 0) {
|
|
24127
|
+
serialized.tags = effect.tags;
|
|
24128
|
+
}
|
|
24129
|
+
if (effect.className !== void 0) {
|
|
24130
|
+
serialized.className = effect.className;
|
|
24131
|
+
}
|
|
24132
|
+
if (effect.static !== void 0) {
|
|
24133
|
+
serialized.static = effect.static;
|
|
24134
|
+
}
|
|
24135
|
+
if (effect.versionSelector !== void 0) {
|
|
24136
|
+
serialized.versionSelector = effect.versionSelector;
|
|
24137
|
+
}
|
|
24138
|
+
if (effect.metamodels !== void 0) {
|
|
24139
|
+
serialized.metamodels = effect.metamodels;
|
|
24140
|
+
}
|
|
24141
|
+
return serialized;
|
|
22341
24142
|
}
|
|
22342
24143
|
async publishSandboxEffectCatalog(host) {
|
|
22343
24144
|
const effects2 = Array.from(
|
|
22344
24145
|
this.getSandboxEffectMap(host.sandboxId).values()
|
|
22345
24146
|
).map((effect) => this.serializeEffect(effect));
|
|
22346
|
-
const result = await
|
|
22347
|
-
effects
|
|
22348
|
-
|
|
24147
|
+
const result = await withTimeout(
|
|
24148
|
+
host.wsClient.call("effects.publishCatalog", {
|
|
24149
|
+
effects: effects2
|
|
24150
|
+
}),
|
|
24151
|
+
EFFECT_CATALOG_SYNC_TIMEOUT_MS,
|
|
24152
|
+
`effects.publishCatalog for sandbox ${host.sandboxId}`
|
|
24153
|
+
);
|
|
22349
24154
|
const acceptedCount = typeof result?.acceptedCount === "number" ? result.acceptedCount : 0;
|
|
22350
24155
|
const rejected = Array.isArray(result?.rejected) ? result.rejected : [];
|
|
22351
24156
|
if (acceptedCount === 0 && rejected.length > 0) {
|
|
@@ -22364,8 +24169,26 @@ var Granular = class _Granular {
|
|
|
22364
24169
|
}
|
|
22365
24170
|
}
|
|
22366
24171
|
async syncSandboxEffectCatalog(sandboxId) {
|
|
22367
|
-
|
|
22368
|
-
|
|
24172
|
+
let lastError;
|
|
24173
|
+
for (let attempt = 1; attempt <= EFFECT_CATALOG_SYNC_RETRY_COUNT; attempt += 1) {
|
|
24174
|
+
try {
|
|
24175
|
+
const host = await this.ensureSandboxEffectHost(sandboxId);
|
|
24176
|
+
await this.publishSandboxEffectCatalog(host);
|
|
24177
|
+
return;
|
|
24178
|
+
} catch (error2) {
|
|
24179
|
+
lastError = error2;
|
|
24180
|
+
this.disconnectSandboxEffectHost(sandboxId);
|
|
24181
|
+
if (attempt === EFFECT_CATALOG_SYNC_RETRY_COUNT || !isRetryableEffectRegistrationError(error2)) {
|
|
24182
|
+
throw error2;
|
|
24183
|
+
}
|
|
24184
|
+
console.warn(
|
|
24185
|
+
`[Granular] Retrying effect registration for sandbox ${sandboxId} after transient failure (${attempt}/${EFFECT_CATALOG_SYNC_RETRY_COUNT - 1} retries used):`,
|
|
24186
|
+
error2
|
|
24187
|
+
);
|
|
24188
|
+
await sleep(EFFECT_CATALOG_SYNC_RETRY_DELAY_MS * attempt);
|
|
24189
|
+
}
|
|
24190
|
+
}
|
|
24191
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
22369
24192
|
}
|
|
22370
24193
|
recoverEffectHost(host, error2) {
|
|
22371
24194
|
if (host.recovering) {
|
|
@@ -22458,7 +24281,8 @@ var Granular = class _Granular {
|
|
|
22458
24281
|
this.apiUrl,
|
|
22459
24282
|
sandboxId,
|
|
22460
24283
|
effectClientId,
|
|
22461
|
-
clientId
|
|
24284
|
+
clientId,
|
|
24285
|
+
this.effectHostUrl
|
|
22462
24286
|
),
|
|
22463
24287
|
sessionId: `effect-host:${effectClientId}`,
|
|
22464
24288
|
token: this.apiKey,
|
|
@@ -22494,7 +24318,11 @@ var Granular = class _Granular {
|
|
|
22494
24318
|
wsClient.on("disconnect", () => {
|
|
22495
24319
|
this.stopEffectHostHeartbeat(host);
|
|
22496
24320
|
});
|
|
22497
|
-
await
|
|
24321
|
+
await withTimeout(
|
|
24322
|
+
wsClient.connect(),
|
|
24323
|
+
EFFECT_HOST_CONNECT_TIMEOUT_MS,
|
|
24324
|
+
`effect host WebSocket connect for sandbox ${sandboxId}`
|
|
24325
|
+
);
|
|
22498
24326
|
await this.synchronizeEffectHost(host);
|
|
22499
24327
|
this.sandboxEffectHosts.set(sandboxId, host);
|
|
22500
24328
|
return host;
|
|
@@ -22617,7 +24445,7 @@ var Granular = class _Granular {
|
|
|
22617
24445
|
/**
|
|
22618
24446
|
* Ensure a permission profile exists for a sandbox, creating it if needed.
|
|
22619
24447
|
* If profileName matches an existing profile name, returns its ID.
|
|
22620
|
-
* Otherwise, creates a
|
|
24448
|
+
* Otherwise, creates a v1 source-profile file shape with an allow default.
|
|
22621
24449
|
*/
|
|
22622
24450
|
async ensurePermissionProfile(sandboxId, profileName) {
|
|
22623
24451
|
try {
|
|
@@ -22631,8 +24459,11 @@ var Granular = class _Granular {
|
|
|
22631
24459
|
const created = await this.permissionProfiles.create(sandboxId, {
|
|
22632
24460
|
name: profileName,
|
|
22633
24461
|
rules: {
|
|
22634
|
-
|
|
22635
|
-
|
|
24462
|
+
schemaVersion: 1,
|
|
24463
|
+
name: profileName,
|
|
24464
|
+
description: profileName === "allow-all" ? "Every declared action is visible unless a manifest policy denies it." : `Generated permission profile ${profileName}`,
|
|
24465
|
+
defaults: { actionPolicy: "allow" },
|
|
24466
|
+
actions: []
|
|
22636
24467
|
}
|
|
22637
24468
|
});
|
|
22638
24469
|
return created.permissionProfileId;
|
|
@@ -22705,33 +24536,63 @@ var Granular = class _Granular {
|
|
|
22705
24536
|
* Permission Profile management for sandboxes
|
|
22706
24537
|
*/
|
|
22707
24538
|
get permissionProfiles() {
|
|
24539
|
+
const profileSourceFromRecord = (record) => {
|
|
24540
|
+
const profile = record.profile || record.rules || {};
|
|
24541
|
+
return {
|
|
24542
|
+
...profile,
|
|
24543
|
+
schemaVersion: profile.schemaVersion || 1,
|
|
24544
|
+
name: profile.name || record.name,
|
|
24545
|
+
description: profile.description || record.description
|
|
24546
|
+
};
|
|
24547
|
+
};
|
|
22708
24548
|
return {
|
|
22709
24549
|
list: async (sandboxId) => {
|
|
22710
24550
|
const result = await this.request(
|
|
22711
|
-
`/control/sandboxes/${sandboxId}/permission-
|
|
24551
|
+
`/control/sandboxes/${sandboxId}/permission-profile-sources`
|
|
22712
24552
|
);
|
|
22713
24553
|
return result.items;
|
|
22714
24554
|
},
|
|
22715
24555
|
get: async (sandboxId, profileId) => {
|
|
22716
|
-
|
|
22717
|
-
`/control/sandboxes/${sandboxId}/permission-
|
|
24556
|
+
const result = await this.request(
|
|
24557
|
+
`/control/sandboxes/${sandboxId}/permission-profile-sources`
|
|
24558
|
+
);
|
|
24559
|
+
const profile = result.items.find(
|
|
24560
|
+
(item) => item.permissionProfileId === profileId || item.name === profileId
|
|
22718
24561
|
);
|
|
24562
|
+
if (!profile) {
|
|
24563
|
+
throw new Error(`Permission profile source not found: ${profileId}`);
|
|
24564
|
+
}
|
|
24565
|
+
return profile;
|
|
22719
24566
|
},
|
|
22720
24567
|
create: async (sandboxId, data) => {
|
|
22721
|
-
|
|
22722
|
-
|
|
24568
|
+
const profile = {
|
|
24569
|
+
...data.rules,
|
|
24570
|
+
schemaVersion: 1,
|
|
24571
|
+
name: data.name
|
|
24572
|
+
};
|
|
24573
|
+
const existingProfiles = await this.permissionProfiles.list(sandboxId);
|
|
24574
|
+
const profiles = [
|
|
24575
|
+
...existingProfiles.filter((existing) => existing.name !== data.name).map((existing) => profileSourceFromRecord(existing)),
|
|
24576
|
+
profile
|
|
24577
|
+
];
|
|
24578
|
+
const result = await this.request(
|
|
24579
|
+
`/control/sandboxes/${sandboxId}/permission-profile-sources`,
|
|
22723
24580
|
{
|
|
22724
|
-
method: "
|
|
22725
|
-
body: JSON.stringify(
|
|
24581
|
+
method: "PUT",
|
|
24582
|
+
body: JSON.stringify({ profiles })
|
|
22726
24583
|
}
|
|
22727
24584
|
);
|
|
24585
|
+
const synced = result.items.find((item) => item.name === data.name) || result.items[0];
|
|
24586
|
+
if (!synced) {
|
|
24587
|
+
throw new Error(
|
|
24588
|
+
`Permission profile source sync did not return ${data.name}`
|
|
24589
|
+
);
|
|
24590
|
+
}
|
|
24591
|
+
return synced;
|
|
22728
24592
|
},
|
|
22729
|
-
delete: async (
|
|
22730
|
-
|
|
22731
|
-
|
|
22732
|
-
{
|
|
22733
|
-
method: "DELETE"
|
|
22734
|
-
}
|
|
24593
|
+
delete: async (_sandboxId, _profileId) => {
|
|
24594
|
+
throw new Error(
|
|
24595
|
+
"Permission profile sources are updated by syncing the desired source set."
|
|
22735
24596
|
);
|
|
22736
24597
|
}
|
|
22737
24598
|
};
|
|
@@ -22944,8 +24805,8 @@ var Granular = class _Granular {
|
|
|
22944
24805
|
/**
|
|
22945
24806
|
* Make an authenticated API request
|
|
22946
24807
|
*/
|
|
22947
|
-
async request(
|
|
22948
|
-
const url = `${this.httpUrl}${
|
|
24808
|
+
async request(path7, options = {}) {
|
|
24809
|
+
const url = `${this.httpUrl}${path7}`;
|
|
22949
24810
|
if (this.debugHttp) {
|
|
22950
24811
|
console.log(`[SDK] Requesting: ${url}`);
|
|
22951
24812
|
}
|
|
@@ -22989,7 +24850,7 @@ var Granular = class _Granular {
|
|
|
22989
24850
|
// src/cli/commands/runtime-shared.ts
|
|
22990
24851
|
function normalizePermissions(raw) {
|
|
22991
24852
|
const values = raw?.split(",").map((value) => value.trim()).filter(Boolean);
|
|
22992
|
-
return values && values.length > 0 ? values : ["
|
|
24853
|
+
return values && values.length > 0 ? values : ["allow-all"];
|
|
22993
24854
|
}
|
|
22994
24855
|
function createGranularClient() {
|
|
22995
24856
|
const config = resolveConfig({ requireApiKey: true });
|
|
@@ -23049,7 +24910,7 @@ async function resolveEnvironmentData(granular, options) {
|
|
|
23049
24910
|
ontology: ontologyId,
|
|
23050
24911
|
environment: environmentName,
|
|
23051
24912
|
userId: options.userId ?? "granular-cli",
|
|
23052
|
-
permissions: options.permissions ?? ["
|
|
24913
|
+
permissions: options.permissions ?? ["allow-all"]
|
|
23053
24914
|
});
|
|
23054
24915
|
return await granular.environments.get(connection.environmentId);
|
|
23055
24916
|
}
|
|
@@ -23089,7 +24950,7 @@ async function connectRuntime(options) {
|
|
|
23089
24950
|
ontology: ontologyId,
|
|
23090
24951
|
environment: options.environment ?? "dev",
|
|
23091
24952
|
userId: options.userId ?? "granular-cli",
|
|
23092
|
-
permissions: options.permissions ?? ["
|
|
24953
|
+
permissions: options.permissions ?? ["allow-all"]
|
|
23093
24954
|
});
|
|
23094
24955
|
const environment = await environmentHandle.createSession();
|
|
23095
24956
|
return {
|
|
@@ -23250,13 +25111,13 @@ async function sessionCreateCommand(options) {
|
|
|
23250
25111
|
printHeader();
|
|
23251
25112
|
}
|
|
23252
25113
|
const { granular } = createGranularClient();
|
|
23253
|
-
const
|
|
25114
|
+
const permissions2 = normalizePermissions(options.permissions);
|
|
23254
25115
|
const envData = await resolveEnvironmentData(granular, {
|
|
23255
25116
|
ontology: options.ontology,
|
|
23256
25117
|
environment: requestedEnvironment,
|
|
23257
25118
|
environmentId: options.environmentId,
|
|
23258
25119
|
userId: options.user,
|
|
23259
|
-
permissions,
|
|
25120
|
+
permissions: permissions2,
|
|
23260
25121
|
createIfMissing: true
|
|
23261
25122
|
});
|
|
23262
25123
|
const environment = await granular.createSession({
|
|
@@ -23581,6 +25442,101 @@ async function jobRunCommand(options) {
|
|
|
23581
25442
|
);
|
|
23582
25443
|
}
|
|
23583
25444
|
|
|
25445
|
+
// src/cli/commands/permissions.ts
|
|
25446
|
+
function parseJsonObject(value) {
|
|
25447
|
+
if (!value) return {};
|
|
25448
|
+
const parsed = JSON.parse(value);
|
|
25449
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
25450
|
+
throw new Error("Expected a JSON object");
|
|
25451
|
+
}
|
|
25452
|
+
return parsed;
|
|
25453
|
+
}
|
|
25454
|
+
function resolveEffectKey(manifest, action, on) {
|
|
25455
|
+
const effects2 = extractManifestEffects(manifest);
|
|
25456
|
+
const matches = effects2.filter((effect) => {
|
|
25457
|
+
if (effect.name !== action) return false;
|
|
25458
|
+
return on ? effect.attachedClass === on : !effect.attachedClass;
|
|
25459
|
+
});
|
|
25460
|
+
if (matches.length === 1) return matches[0].effectKey;
|
|
25461
|
+
if (matches.length === 0) {
|
|
25462
|
+
throw new Error(
|
|
25463
|
+
on ? `Action "${action}" on "${on}" was not found.` : `Global action "${action}" was not found.`
|
|
25464
|
+
);
|
|
25465
|
+
}
|
|
25466
|
+
throw new Error(`Action "${action}" is ambiguous. Add --on.`);
|
|
25467
|
+
}
|
|
25468
|
+
async function permissionsValidateCommand() {
|
|
25469
|
+
const config = resolveConfig({ requireProject: true });
|
|
25470
|
+
const manifest = config.project.manifest;
|
|
25471
|
+
const profiles = readPermissionProfileFiles();
|
|
25472
|
+
const result = await compilePolicyBundle({
|
|
25473
|
+
sandboxId: config.sandboxId,
|
|
25474
|
+
effects: extractManifestEffects(manifest),
|
|
25475
|
+
profileSources: profiles
|
|
25476
|
+
});
|
|
25477
|
+
if (result.errors.length > 0) {
|
|
25478
|
+
error("Permission policy validation failed:");
|
|
25479
|
+
for (const error2 of result.errors) error(` ${error2}`);
|
|
25480
|
+
process.exit(1);
|
|
25481
|
+
}
|
|
25482
|
+
success(
|
|
25483
|
+
`Validated ${Object.keys(result.bundle.profiles).length} permission profile(s).`
|
|
25484
|
+
);
|
|
25485
|
+
if (result.warnings.length > 0) {
|
|
25486
|
+
for (const warning of result.warnings) warn(warning);
|
|
25487
|
+
}
|
|
25488
|
+
}
|
|
25489
|
+
async function permissionsPreviewCommand(options) {
|
|
25490
|
+
const config = resolveConfig({
|
|
25491
|
+
requireApiKey: Boolean(options.versionId),
|
|
25492
|
+
requireProject: true
|
|
25493
|
+
});
|
|
25494
|
+
if (!options.profile || !options.action) {
|
|
25495
|
+
throw new Error("permissions preview requires --profile and --action");
|
|
25496
|
+
}
|
|
25497
|
+
const requestInput = parseJsonObject(options.input);
|
|
25498
|
+
const objectContext = parseJsonObject(options.object);
|
|
25499
|
+
const stateMachines = parseJsonObject(options.stateMachines);
|
|
25500
|
+
const effectKey = resolveEffectKey(
|
|
25501
|
+
config.project.manifest,
|
|
25502
|
+
options.action,
|
|
25503
|
+
options.on
|
|
25504
|
+
);
|
|
25505
|
+
if (options.versionId) {
|
|
25506
|
+
const api = new ApiClient(config.apiKey, config.apiUrl);
|
|
25507
|
+
const decision2 = await api.evaluateVersionPolicy(options.versionId, {
|
|
25508
|
+
permissionProfileName: options.profile,
|
|
25509
|
+
effectKey,
|
|
25510
|
+
input: requestInput,
|
|
25511
|
+
object: objectContext,
|
|
25512
|
+
stateMachines
|
|
25513
|
+
});
|
|
25514
|
+
keyValue({
|
|
25515
|
+
Decision: decision2.outcome,
|
|
25516
|
+
Matched: (decision2.matches || []).map((match) => match.reason || match.summary || match.id).join("; ") || "none"
|
|
25517
|
+
});
|
|
25518
|
+
return;
|
|
25519
|
+
}
|
|
25520
|
+
const result = await compilePolicyBundle({
|
|
25521
|
+
sandboxId: config.sandboxId,
|
|
25522
|
+
effects: extractManifestEffects(config.project.manifest),
|
|
25523
|
+
profileSources: readPermissionProfileFiles()
|
|
25524
|
+
});
|
|
25525
|
+
if (result.errors.length > 0) {
|
|
25526
|
+
throw new Error(result.errors.join("; "));
|
|
25527
|
+
}
|
|
25528
|
+
const decision = evaluateCompiledPolicyBundle({
|
|
25529
|
+
bundle: result.bundle,
|
|
25530
|
+
profileName: options.profile,
|
|
25531
|
+
effectKey,
|
|
25532
|
+
context: { input: requestInput, object: objectContext, stateMachines }
|
|
25533
|
+
});
|
|
25534
|
+
keyValue({
|
|
25535
|
+
Decision: decision.outcome,
|
|
25536
|
+
Matched: decision.matches.map((match) => match.reason || match.summary || match.id).join("; ") || "none"
|
|
25537
|
+
});
|
|
25538
|
+
}
|
|
25539
|
+
|
|
23584
25540
|
// src/cli/commands/version.ts
|
|
23585
25541
|
function formatVersionLabel(versionId, versionNumber) {
|
|
23586
25542
|
return versionNumber != null ? `v${versionNumber}` : versionId;
|
|
@@ -23822,67 +25778,130 @@ program2.hook("preAction", () => {
|
|
|
23822
25778
|
process.env.GRANULAR_ENDPOINT_MODE = mode;
|
|
23823
25779
|
}
|
|
23824
25780
|
});
|
|
23825
|
-
program2.command("init [project-name]").description("Initialize a new Granular project").option("--skip-build", "Skip the initial build step").option(
|
|
23826
|
-
|
|
23827
|
-
|
|
23828
|
-
|
|
23829
|
-
|
|
23830
|
-
|
|
23831
|
-
|
|
23832
|
-
|
|
23833
|
-
|
|
23834
|
-
|
|
23835
|
-
|
|
23836
|
-
|
|
23837
|
-
|
|
23838
|
-
|
|
23839
|
-
|
|
23840
|
-
|
|
23841
|
-
|
|
23842
|
-
|
|
23843
|
-
|
|
23844
|
-
|
|
25781
|
+
program2.command("init [project-name]").description("Initialize a new Granular project").option("--skip-build", "Skip the initial build step").option(
|
|
25782
|
+
"--template <template>",
|
|
25783
|
+
"Starter ontology template: library|support|delivery"
|
|
25784
|
+
).option(
|
|
25785
|
+
"--agent-docs",
|
|
25786
|
+
"Add AGENTS.md and docs/granular-manifest.md (for AI coding agents)"
|
|
25787
|
+
).option("--no-agent-docs", "Skip agent documentation files").option("--cursor-hooks", "Emit optional Cursor hook files under .cursor/").option(
|
|
25788
|
+
"--cursor-subagents",
|
|
25789
|
+
"Emit optional Cursor subagent files under .cursor/"
|
|
25790
|
+
).option("--cursor-all", "Emit both Cursor hooks and Cursor subagents").option(
|
|
25791
|
+
"--control-plane-session <sessionId>",
|
|
25792
|
+
"Attach the init flow to a control-plane onboarding session"
|
|
25793
|
+
).option("--project-mode <mode>", "Onboarding context: new|existing").option(
|
|
25794
|
+
"--auth-strategy <strategy>",
|
|
25795
|
+
"Onboarding context: just-me|granular-auth|existing-auth"
|
|
25796
|
+
).option("--ide <ide>", "Onboarding context: cursor|codex|claude-code").option("--data-strategy <strategy>", "Onboarding context: demo|real").option(
|
|
25797
|
+
"--product-summary <text>",
|
|
25798
|
+
"Onboarding context: product brief from the control plane"
|
|
25799
|
+
).option(
|
|
25800
|
+
"--roles-summary <text>",
|
|
25801
|
+
"Onboarding context: roles and permission notes from the control plane"
|
|
25802
|
+
).action(
|
|
25803
|
+
async (projectName, opts) => {
|
|
25804
|
+
try {
|
|
25805
|
+
await initCommand(projectName, {
|
|
25806
|
+
skipBuild: opts.skipBuild,
|
|
25807
|
+
template: opts.template,
|
|
25808
|
+
agentDocs: opts.agentDocs,
|
|
25809
|
+
noAgentDocs: opts.noAgentDocs,
|
|
25810
|
+
cursorHooks: opts.cursorHooks || opts.cursorAll,
|
|
25811
|
+
cursorSubagents: opts.cursorSubagents || opts.cursorAll,
|
|
25812
|
+
controlPlaneSession: opts.controlPlaneSession,
|
|
25813
|
+
projectMode: opts.projectMode,
|
|
25814
|
+
authStrategy: opts.authStrategy,
|
|
25815
|
+
ide: opts.ide,
|
|
25816
|
+
dataStrategy: opts.dataStrategy,
|
|
25817
|
+
productSummary: opts.productSummary,
|
|
25818
|
+
rolesSummary: opts.rolesSummary
|
|
25819
|
+
});
|
|
25820
|
+
} catch (err) {
|
|
25821
|
+
error(err.message);
|
|
25822
|
+
process.exit(1);
|
|
25823
|
+
}
|
|
23845
25824
|
}
|
|
23846
|
-
|
|
23847
|
-
program2.command("login").description("Authenticate with Granular (browser by default)").option("--manual", "Prompt for API key instead of browser login").option("--api-key <key>", "Use a provided API key directly").option(
|
|
23848
|
-
|
|
23849
|
-
|
|
23850
|
-
|
|
25825
|
+
);
|
|
25826
|
+
program2.command("login").description("Authenticate with Granular (browser by default)").option("--manual", "Prompt for API key instead of browser login").option("--api-key <key>", "Use a provided API key directly").option(
|
|
25827
|
+
"--timeout <seconds>",
|
|
25828
|
+
"Browser auth timeout in seconds",
|
|
25829
|
+
(value) => {
|
|
25830
|
+
const n = Number.parseInt(value, 10);
|
|
25831
|
+
return Number.isFinite(n) ? n : 180;
|
|
25832
|
+
}
|
|
25833
|
+
).action(
|
|
25834
|
+
async (opts) => {
|
|
25835
|
+
try {
|
|
25836
|
+
await loginCommand({
|
|
25837
|
+
manual: opts.manual,
|
|
25838
|
+
apiKey: opts.apiKey,
|
|
25839
|
+
timeout: opts.timeout
|
|
25840
|
+
});
|
|
25841
|
+
} catch (err) {
|
|
25842
|
+
error(err.message);
|
|
25843
|
+
process.exit(1);
|
|
25844
|
+
}
|
|
25845
|
+
}
|
|
25846
|
+
);
|
|
25847
|
+
program2.command("pull <version-id>").description("Pull the manifest for a specific ontology version").action(async (versionId) => {
|
|
23851
25848
|
try {
|
|
23852
|
-
await
|
|
23853
|
-
manual: opts.manual,
|
|
23854
|
-
apiKey: opts.apiKey,
|
|
23855
|
-
timeout: opts.timeout
|
|
23856
|
-
});
|
|
25849
|
+
await pullCommand(versionId);
|
|
23857
25850
|
} catch (err) {
|
|
23858
25851
|
error(err.message);
|
|
23859
25852
|
process.exit(1);
|
|
23860
25853
|
}
|
|
23861
25854
|
});
|
|
23862
|
-
program2.command("
|
|
25855
|
+
program2.command("build").description(
|
|
25856
|
+
"Create or reuse the ontology version for your current manifest, then run its build"
|
|
25857
|
+
).action(async () => {
|
|
23863
25858
|
try {
|
|
23864
|
-
await
|
|
25859
|
+
await buildCommand();
|
|
23865
25860
|
} catch (err) {
|
|
23866
25861
|
error(err.message);
|
|
23867
25862
|
process.exit(1);
|
|
23868
25863
|
}
|
|
23869
25864
|
});
|
|
23870
|
-
program2.command("
|
|
25865
|
+
program2.command("deploy").description(
|
|
25866
|
+
"Push the current revision to dev; use --prod to move prod there too"
|
|
25867
|
+
).option("--prod", "Also point prod to the current version").action(async (opts) => {
|
|
23871
25868
|
try {
|
|
23872
|
-
await
|
|
25869
|
+
await deployCommand({ prod: opts.prod });
|
|
23873
25870
|
} catch (err) {
|
|
23874
25871
|
error(err.message);
|
|
23875
25872
|
process.exit(1);
|
|
23876
25873
|
}
|
|
23877
25874
|
});
|
|
23878
|
-
program2.command("
|
|
25875
|
+
var permissions = program2.command("permissions").description("Validate and preview permission profile files");
|
|
25876
|
+
permissions.command("validate").description("Validate permissions/*.json against granular.json").action(async () => {
|
|
23879
25877
|
try {
|
|
23880
|
-
await
|
|
25878
|
+
await permissionsValidateCommand();
|
|
23881
25879
|
} catch (err) {
|
|
23882
25880
|
error(err.message);
|
|
23883
25881
|
process.exit(1);
|
|
23884
25882
|
}
|
|
23885
25883
|
});
|
|
25884
|
+
permissions.command("preview").description("Preview a policy decision for a profile and action").requiredOption("--profile <profile>", "Permission profile name").requiredOption("--action <action>", "Action name").option("--on <className>", "Class name for instance/static actions").option("--input <json>", "Action input JSON object", "{}").option(
|
|
25885
|
+
"--object <json>",
|
|
25886
|
+
"Target object fields JSON object for object-based conditions",
|
|
25887
|
+
"{}"
|
|
25888
|
+
).option(
|
|
25889
|
+
"--state-machines <json>",
|
|
25890
|
+
'Current state-machine states JSON object, e.g. {"lifecycle":"active"}',
|
|
25891
|
+
"{}"
|
|
25892
|
+
).option(
|
|
25893
|
+
"--version-id <versionId>",
|
|
25894
|
+
"Evaluate against a compiled ontology version"
|
|
25895
|
+
).action(
|
|
25896
|
+
async (opts) => {
|
|
25897
|
+
try {
|
|
25898
|
+
await permissionsPreviewCommand(opts);
|
|
25899
|
+
} catch (err) {
|
|
25900
|
+
error(err.message);
|
|
25901
|
+
process.exit(1);
|
|
25902
|
+
}
|
|
25903
|
+
}
|
|
25904
|
+
);
|
|
23886
25905
|
program2.command("status").description("Show ontology versions, dev/prod, and manifest status").option("--verbose", "Include endpoint/config details helpful for debugging").option("--json", "Print machine-readable JSON").action(async (options) => {
|
|
23887
25906
|
try {
|
|
23888
25907
|
await statusCommand(options);
|
|
@@ -23900,14 +25919,16 @@ version.command("current").description("Show the latest/current ontology version
|
|
|
23900
25919
|
process.exit(1);
|
|
23901
25920
|
}
|
|
23902
25921
|
});
|
|
23903
|
-
version.command("diff <version-id> [against-version-id]").description("Show the semantic diff between one version and another").option("--json", "Print machine-readable JSON").action(
|
|
23904
|
-
|
|
23905
|
-
|
|
23906
|
-
|
|
23907
|
-
|
|
23908
|
-
|
|
25922
|
+
version.command("diff <version-id> [against-version-id]").description("Show the semantic diff between one version and another").option("--json", "Print machine-readable JSON").action(
|
|
25923
|
+
async (versionId, againstVersionId, options) => {
|
|
25924
|
+
try {
|
|
25925
|
+
await versionDiffCommand(versionId, againstVersionId, options);
|
|
25926
|
+
} catch (err) {
|
|
25927
|
+
error(err.message);
|
|
25928
|
+
process.exit(1);
|
|
25929
|
+
}
|
|
23909
25930
|
}
|
|
23910
|
-
|
|
25931
|
+
);
|
|
23911
25932
|
var tag = program2.command("tag").description("Inspect and move release tags such as dev and prod");
|
|
23912
25933
|
tag.command("list").description("List tags for the current ontology").action(async () => {
|
|
23913
25934
|
try {
|
|
@@ -23966,7 +25987,9 @@ program2.command("dev").description("Start development mode (watch + auto-rebuil
|
|
|
23966
25987
|
process.exit(1);
|
|
23967
25988
|
}
|
|
23968
25989
|
});
|
|
23969
|
-
program2.command("document").description(
|
|
25990
|
+
program2.command("document").description(
|
|
25991
|
+
"Generate GRANULAR_SANDBOX.md (agent reference) from granular.json"
|
|
25992
|
+
).action(async () => {
|
|
23970
25993
|
try {
|
|
23971
25994
|
await documentCommand();
|
|
23972
25995
|
} catch (err) {
|
|
@@ -23974,83 +25997,232 @@ program2.command("document").description("Generate GRANULAR_SANDBOX.md (agent re
|
|
|
23974
25997
|
process.exit(1);
|
|
23975
25998
|
}
|
|
23976
25999
|
});
|
|
23977
|
-
program2.command("simulate [ontology-id]").description(
|
|
23978
|
-
|
|
23979
|
-
|
|
23980
|
-
|
|
23981
|
-
|
|
23982
|
-
|
|
26000
|
+
program2.command("simulate [ontology-id]").description(
|
|
26001
|
+
"Open the Granular simulator in the browser for the current (or given) ontology"
|
|
26002
|
+
).option(
|
|
26003
|
+
"--subject-id <subjectId>",
|
|
26004
|
+
"Open the simulator preloaded for a specific subject"
|
|
26005
|
+
).option(
|
|
26006
|
+
"--environment <environment>",
|
|
26007
|
+
"Add an environment query parameter to the simulator URL"
|
|
26008
|
+
).option(
|
|
26009
|
+
"--session <sessionId>",
|
|
26010
|
+
"Add a session query parameter to the simulator URL"
|
|
26011
|
+
).option("--user <userId>", "Add a user query parameter to the simulator URL").option("--print-url", "Print the simulator URL without opening a browser").action(
|
|
26012
|
+
async (sandboxId, options) => {
|
|
26013
|
+
try {
|
|
26014
|
+
await simulateCommand(sandboxId, options);
|
|
26015
|
+
} catch (err) {
|
|
26016
|
+
error(err.message);
|
|
26017
|
+
process.exit(1);
|
|
26018
|
+
}
|
|
23983
26019
|
}
|
|
23984
|
-
|
|
26020
|
+
);
|
|
23985
26021
|
var connect = program2.command("connect").description("Run connectivity checks against a live Granular ontology");
|
|
23986
|
-
connect.command("test").description("Create a real session and verify connect/disconnect works").option(
|
|
23987
|
-
|
|
23988
|
-
|
|
23989
|
-
|
|
23990
|
-
|
|
23991
|
-
|
|
26022
|
+
connect.command("test").description("Create a real session and verify connect/disconnect works").option(
|
|
26023
|
+
"--ontology <ontologyId>",
|
|
26024
|
+
"Override the ontology id from .granularrc"
|
|
26025
|
+
).option(
|
|
26026
|
+
"--environment <environment>",
|
|
26027
|
+
"Environment slot to connect to",
|
|
26028
|
+
"dev"
|
|
26029
|
+
).option("--user <userId>", "External user id to connect as").option(
|
|
26030
|
+
"--permissions <list>",
|
|
26031
|
+
"Comma-separated permission profile to ensure",
|
|
26032
|
+
"allow-all"
|
|
26033
|
+
).option("--json", "Print machine-readable JSON").action(
|
|
26034
|
+
async (options) => {
|
|
26035
|
+
try {
|
|
26036
|
+
await connectTestCommand(options);
|
|
26037
|
+
} catch (err) {
|
|
26038
|
+
error(err.message);
|
|
26039
|
+
process.exit(1);
|
|
26040
|
+
}
|
|
23992
26041
|
}
|
|
23993
|
-
|
|
23994
|
-
var session = program2.command("session").description(
|
|
26042
|
+
);
|
|
26043
|
+
var session = program2.command("session").description(
|
|
26044
|
+
"Inspect live Granular sessions and their Automerge-backed state"
|
|
26045
|
+
);
|
|
23995
26046
|
for (const [name, description, handler] of [
|
|
23996
26047
|
["heap", "Print the normalized session heap", sessionHeapCommand],
|
|
23997
26048
|
["doc", "Print the full session document", sessionDocCommand]
|
|
23998
26049
|
]) {
|
|
23999
|
-
session.command(name).description(description).option(
|
|
26050
|
+
session.command(name).description(description).option(
|
|
26051
|
+
"--ontology <ontologyId>",
|
|
26052
|
+
"Override the ontology id from .granularrc"
|
|
26053
|
+
).option(
|
|
26054
|
+
"--environment <environment>",
|
|
26055
|
+
"Environment slot to connect to when no session id is provided",
|
|
26056
|
+
"dev"
|
|
26057
|
+
).option(
|
|
26058
|
+
"--user <userId>",
|
|
26059
|
+
"External user id to connect as when no session id is provided"
|
|
26060
|
+
).option(
|
|
26061
|
+
"--permissions <list>",
|
|
26062
|
+
"Comma-separated permission profile to ensure",
|
|
26063
|
+
"allow-all"
|
|
26064
|
+
).option(
|
|
26065
|
+
"--session <sessionId>",
|
|
26066
|
+
"Connect to an existing session instead of creating a fresh one"
|
|
26067
|
+
).option("--json", "Print machine-readable JSON").action(
|
|
26068
|
+
async (options) => {
|
|
26069
|
+
try {
|
|
26070
|
+
await handler(options);
|
|
26071
|
+
} catch (err) {
|
|
26072
|
+
error(err.message);
|
|
26073
|
+
process.exit(1);
|
|
26074
|
+
}
|
|
26075
|
+
}
|
|
26076
|
+
);
|
|
26077
|
+
}
|
|
26078
|
+
session.command("create").description(
|
|
26079
|
+
"Create a new session for an existing environment or environment slot"
|
|
26080
|
+
).option(
|
|
26081
|
+
"--ontology <ontologyId>",
|
|
26082
|
+
"Override the ontology id from .granularrc"
|
|
26083
|
+
).option(
|
|
26084
|
+
"--environment <environment>",
|
|
26085
|
+
"Environment slot name to resolve when --environment-id is not provided",
|
|
26086
|
+
"dev"
|
|
26087
|
+
).option(
|
|
26088
|
+
"--environment-id <environmentId>",
|
|
26089
|
+
"Create a session for this exact environment id"
|
|
26090
|
+
).option(
|
|
26091
|
+
"--user <userId>",
|
|
26092
|
+
"External user id to connect as when a named environment needs to be created"
|
|
26093
|
+
).option(
|
|
26094
|
+
"--permissions <list>",
|
|
26095
|
+
"Comma-separated permission profile to ensure when a named environment needs to be created",
|
|
26096
|
+
"allow-all"
|
|
26097
|
+
).option("--json", "Print machine-readable JSON").action(
|
|
26098
|
+
async (options) => {
|
|
24000
26099
|
try {
|
|
24001
|
-
await
|
|
26100
|
+
await sessionCreateCommand(options);
|
|
24002
26101
|
} catch (err) {
|
|
24003
26102
|
error(err.message);
|
|
24004
26103
|
process.exit(1);
|
|
24005
26104
|
}
|
|
24006
|
-
});
|
|
24007
|
-
}
|
|
24008
|
-
session.command("create").description("Create a new session for an existing environment or environment slot").option("--ontology <ontologyId>", "Override the ontology id from .granularrc").option("--environment <environment>", "Environment slot name to resolve when --environment-id is not provided", "dev").option("--environment-id <environmentId>", "Create a session for this exact environment id").option("--user <userId>", "External user id to connect as when a named environment needs to be created").option("--permissions <list>", "Comma-separated permission profiles to ensure when a named environment needs to be created", "default").option("--json", "Print machine-readable JSON").action(async (options) => {
|
|
24009
|
-
try {
|
|
24010
|
-
await sessionCreateCommand(options);
|
|
24011
|
-
} catch (err) {
|
|
24012
|
-
error(err.message);
|
|
24013
|
-
process.exit(1);
|
|
24014
26105
|
}
|
|
24015
|
-
|
|
24016
|
-
session.command("list").description("List indexed sessions for an environment").option(
|
|
24017
|
-
|
|
24018
|
-
|
|
24019
|
-
|
|
24020
|
-
|
|
24021
|
-
|
|
26106
|
+
);
|
|
26107
|
+
session.command("list").description("List indexed sessions for an environment").option(
|
|
26108
|
+
"--ontology <ontologyId>",
|
|
26109
|
+
"Override the ontology id from .granularrc"
|
|
26110
|
+
).option(
|
|
26111
|
+
"--environment <environment>",
|
|
26112
|
+
"Environment slot name to resolve when --environment-id is not provided",
|
|
26113
|
+
"dev"
|
|
26114
|
+
).option(
|
|
26115
|
+
"--environment-id <environmentId>",
|
|
26116
|
+
"List sessions for this exact environment id"
|
|
26117
|
+
).option(
|
|
26118
|
+
"--status <status>",
|
|
26119
|
+
"Session status filter: active|closed|all",
|
|
26120
|
+
"active"
|
|
26121
|
+
).option("--json", "Print machine-readable JSON").action(
|
|
26122
|
+
async (options) => {
|
|
26123
|
+
try {
|
|
26124
|
+
await sessionListCommand(options);
|
|
26125
|
+
} catch (err) {
|
|
26126
|
+
error(err.message);
|
|
26127
|
+
process.exit(1);
|
|
26128
|
+
}
|
|
24022
26129
|
}
|
|
24023
|
-
|
|
26130
|
+
);
|
|
24024
26131
|
var job = program2.command("job").description("Run real Granular runtime jobs from the CLI");
|
|
24025
|
-
job.command("run").description(
|
|
24026
|
-
|
|
24027
|
-
|
|
24028
|
-
|
|
24029
|
-
|
|
24030
|
-
|
|
26132
|
+
job.command("run").description(
|
|
26133
|
+
"Submit a job from a file or inline code and wait for the result"
|
|
26134
|
+
).option(
|
|
26135
|
+
"--ontology <ontologyId>",
|
|
26136
|
+
"Override the ontology id from .granularrc"
|
|
26137
|
+
).option(
|
|
26138
|
+
"--environment <environment>",
|
|
26139
|
+
"Environment slot to connect to when no session id is provided",
|
|
26140
|
+
"dev"
|
|
26141
|
+
).option(
|
|
26142
|
+
"--user <userId>",
|
|
26143
|
+
"External user id to connect as when no session id is provided"
|
|
26144
|
+
).option(
|
|
26145
|
+
"--permissions <list>",
|
|
26146
|
+
"Comma-separated permission profile to ensure",
|
|
26147
|
+
"allow-all"
|
|
26148
|
+
).option(
|
|
26149
|
+
"--session <sessionId>",
|
|
26150
|
+
"Connect to an existing session instead of creating a fresh one"
|
|
26151
|
+
).option("--file <path>", "Read the job source from a file").option("--inline <code>", "Provide the job source inline").option("--json", "Print machine-readable JSON").action(
|
|
26152
|
+
async (options) => {
|
|
26153
|
+
try {
|
|
26154
|
+
await jobRunCommand(options);
|
|
26155
|
+
} catch (err) {
|
|
26156
|
+
error(err.message);
|
|
26157
|
+
process.exit(1);
|
|
26158
|
+
}
|
|
24031
26159
|
}
|
|
24032
|
-
|
|
24033
|
-
program2.command("graphql").description(
|
|
24034
|
-
|
|
24035
|
-
|
|
24036
|
-
|
|
24037
|
-
|
|
24038
|
-
|
|
26160
|
+
);
|
|
26161
|
+
program2.command("graphql").description(
|
|
26162
|
+
"Execute a real GraphQL query against the current Granular environment"
|
|
26163
|
+
).requiredOption("--query <query>", "GraphQL query or mutation string").option("--variables <json>", "JSON object of GraphQL variables").option(
|
|
26164
|
+
"--ontology <ontologyId>",
|
|
26165
|
+
"Override the ontology id from .granularrc"
|
|
26166
|
+
).option(
|
|
26167
|
+
"--environment <environment>",
|
|
26168
|
+
"Environment slot to connect to when no session id is provided",
|
|
26169
|
+
"dev"
|
|
26170
|
+
).option(
|
|
26171
|
+
"--user <userId>",
|
|
26172
|
+
"External user id to connect as when no session id is provided"
|
|
26173
|
+
).option(
|
|
26174
|
+
"--permissions <list>",
|
|
26175
|
+
"Comma-separated permission profile to ensure",
|
|
26176
|
+
"allow-all"
|
|
26177
|
+
).option(
|
|
26178
|
+
"--session <sessionId>",
|
|
26179
|
+
"Connect to an existing session instead of creating a fresh one"
|
|
26180
|
+
).option("--json", "Print machine-readable JSON").action(
|
|
26181
|
+
async (options) => {
|
|
26182
|
+
try {
|
|
26183
|
+
await graphqlCommand(options);
|
|
26184
|
+
} catch (err) {
|
|
26185
|
+
error(err.message);
|
|
26186
|
+
process.exit(1);
|
|
26187
|
+
}
|
|
24039
26188
|
}
|
|
24040
|
-
|
|
26189
|
+
);
|
|
24041
26190
|
var effects = program2.command("effects").description("Inspect declared and live Granular effects for an environment");
|
|
24042
26191
|
for (const [name, description, handler] of [
|
|
24043
26192
|
["list", "List the current effect catalog", effectsListCommand],
|
|
24044
|
-
[
|
|
26193
|
+
[
|
|
26194
|
+
"diff",
|
|
26195
|
+
"Compare declared effects to live ready handlers",
|
|
26196
|
+
effectsDiffCommand
|
|
26197
|
+
]
|
|
24045
26198
|
]) {
|
|
24046
|
-
effects.command(name).description(description).option(
|
|
24047
|
-
|
|
24048
|
-
|
|
24049
|
-
|
|
24050
|
-
|
|
24051
|
-
|
|
26199
|
+
effects.command(name).description(description).option(
|
|
26200
|
+
"--ontology <ontologyId>",
|
|
26201
|
+
"Override the ontology id from .granularrc"
|
|
26202
|
+
).option(
|
|
26203
|
+
"--environment <environment>",
|
|
26204
|
+
"Environment slot to connect to when no session id is provided",
|
|
26205
|
+
"dev"
|
|
26206
|
+
).option(
|
|
26207
|
+
"--user <userId>",
|
|
26208
|
+
"External user id to connect as when no session id is provided"
|
|
26209
|
+
).option(
|
|
26210
|
+
"--permissions <list>",
|
|
26211
|
+
"Comma-separated permission profile to ensure",
|
|
26212
|
+
"allow-all"
|
|
26213
|
+
).option(
|
|
26214
|
+
"--session <sessionId>",
|
|
26215
|
+
"Connect to an existing session instead of creating a fresh one"
|
|
26216
|
+
).option("--json", "Print machine-readable JSON").action(
|
|
26217
|
+
async (options) => {
|
|
26218
|
+
try {
|
|
26219
|
+
await handler(options);
|
|
26220
|
+
} catch (err) {
|
|
26221
|
+
error(err.message);
|
|
26222
|
+
process.exit(1);
|
|
26223
|
+
}
|
|
24052
26224
|
}
|
|
24053
|
-
|
|
26225
|
+
);
|
|
24054
26226
|
}
|
|
24055
26227
|
void program2.parseAsync(process.argv).then(() => {
|
|
24056
26228
|
process.exit(0);
|