@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/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 path6 = __require("path");
1198
- var fs7 = __require("fs");
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 (fs7.existsSync(executableFile)) return;
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 = path6.resolve(baseDir, baseName);
2197
- if (fs7.existsSync(localBin)) return localBin;
2198
- if (sourceExt.includes(path6.extname(baseName))) return void 0;
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) => fs7.existsSync(`${localBin}${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 = fs7.realpathSync(this._scriptPath);
2212
+ resolvedScriptPath = fs8.realpathSync(this._scriptPath);
2213
2213
  } catch {
2214
2214
  resolvedScriptPath = this._scriptPath;
2215
2215
  }
2216
- executableDir = path6.resolve(
2217
- path6.dirname(resolvedScriptPath),
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 = path6.basename(
2224
+ const legacyName = path7.basename(
2225
2225
  this._scriptPath,
2226
- path6.extname(this._scriptPath)
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(path6.extname(executableFile));
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 = path6.basename(filename, path6.extname(filename));
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(path7) {
3099
- if (path7 === void 0) return this._executableDir;
3100
- this._executableDir = path7;
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 fs7 = __require("fs");
3444
- var path6 = __require("path");
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 (fs7.existsSync(filepath)) {
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 = path6.resolve(process.cwd(), ".env.vault");
3560
+ possibleVaultPath = path7.resolve(process.cwd(), ".env.vault");
3561
3561
  }
3562
- if (fs7.existsSync(possibleVaultPath)) {
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] === "~" ? path6.join(os2.homedir(), envPath.slice(1)) : envPath;
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 = path6.resolve(process.cwd(), ".env");
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 path7 of optionPaths) {
3609
+ for (const path8 of optionPaths) {
3610
3610
  try {
3611
- const parsed = DotenvModule.parse(fs7.readFileSync(path7, { encoding }));
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 ${path7} ${e.message}`);
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 relative = path6.relative(process.cwd(), filePath);
3631
- shortPaths.push(relative);
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: ["default"]
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: ["default"]
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: ["default"]
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(path6, options = {}) {
6930
- const url = `${this.baseUrl}${path6}`;
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-profiles`,
7993
+ `/control/sandboxes/${sandboxId}/permission-profile-sources`,
7103
7994
  {
7104
- method: "POST",
7105
- body: JSON.stringify(data)
7995
+ method: "PUT",
7996
+ body: JSON.stringify({ profiles })
7106
7997
  }
7107
7998
  );
7108
7999
  }
7109
- async ensureDefaultProfile(sandboxId) {
7110
- try {
7111
- return await this.createPermissionProfile(sandboxId, {
7112
- name: "default",
7113
- rules: {
7114
- effects: { allow: ["*"] },
7115
- resources: { allow: ["*"] }
7116
- }
7117
- });
7118
- } catch {
7119
- const profiles = await this.request(
7120
- `/control/sandboxes/${sandboxId}/permission-profiles`
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: path6, errorMaps, issueData } = params;
9525
- const fullPath = [...path6, ...issueData.path || []];
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, path6, key) {
10528
+ constructor(parent, value, path7, key) {
9642
10529
  this._cachedPath = [];
9643
10530
  this.parent = parent;
9644
10531
  this.data = value;
9645
- this._path = path6;
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 profiles (e.g. \`default\`) that can allow or deny which effects a user may call.
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: ['default'],`);
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: ['default'] })\`.
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
- // src/cli/commands/pull.ts
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
- }, 3e4);
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 value = typeof option === "string" ? option : typeof option?.value === "string" ? option.value : "";
18280
- const label = typeof option === "string" ? option : typeof option?.label === "string" ? option.label : "";
18281
- const description = typeof option === "string" ? "" : typeof option?.description === "string" ? option.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 : void 0,
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
- constructor(client, clientId) {
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, domainRevision) {
18497
- let revision = domainRevision || this.currentDomainRevision || this.extractDomainRevisionFromDoc(this.client.doc) || void 0;
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
- await this.client.call("prompt.answer", {
18554
- promptId,
18555
- answer: resolvedAnswer,
18556
- value: resolvedAnswer
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 this.appendConversationMessage({
18562
- role: "user",
18563
- content,
18564
- promptId
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("conversation.append", input);
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 domainPkg = doc.domain?.packages?.domain;
18581
- if (domainPkg?.tools && Array.isArray(domainPkg.tools)) {
18582
- for (const tool of domainPkg.tools) {
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 message = error2 instanceof Error ? error2.message : String(error2);
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._rejectResult(error2);
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 path6 of list.paths || []) {
19598
- const entry = heap.entriesByPath?.[path6];
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((path6) => sessionHeap.entriesByPath?.[path6]).filter((entry) => Boolean(entry))
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 url = new URL(apiUrl);
20226
- if (url.pathname.endsWith("/granular/ws/connect")) {
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(path6, options = {}) {
22163
+ async controlPlaneRequest(path7, options = {}) {
20532
22164
  const runtimeBase = this.getRuntimeBaseUrl();
20533
- const url = `${runtimeBase}${path6}`;
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 + real-world ID into a unique graph path.
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
- * Format: `{className}_{id}` deterministic, human-readable.
20558
- *
20559
- * **Convention**: class names should be simple identifiers without
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
- * Extract the real-world ID from a graph path, given the class name.
22197
+ * Best-effort extraction of an ID-like suffix from a graph path.
20568
22198
  *
20569
- * Strips the `{className}_` prefix. Returns the raw path if the
20570
- * expected prefix is not found.
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(path6, query) {
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 response = await fetch(
21494
- `${this.environment.runtimeBaseUrl}/orchestrator/ws/sessions/${encodeURIComponent(this.sessionId)}${path6}${queryString ? `?${queryString}` : ""}`,
21495
- {
21496
- method: "GET",
21497
- headers: {
21498
- Authorization: `Bearer ${this.environment.authToken}`,
21499
- "Content-Type": "application/json"
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
- return response.json();
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: (path6) => this.sessionDataRequest(
21556
- `/heap/entries/${encodeURIComponent(path6)}`
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
- if (!resolved.setupTriggerReason) {
22224
- return;
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
- await updateSetupRun({ markHookCompleted: true });
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
- return {
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 host.wsClient.call("effects.publishCatalog", {
22347
- effects: effects2
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
- const host = await this.ensureSandboxEffectHost(sandboxId);
22368
- await this.publishSandboxEffectCatalog(host);
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 wsClient.connect();
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 new profile with default allow-all rules.
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
- effects: { allow: ["*"] },
22635
- resources: { allow: ["*"] }
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-profiles`
24551
+ `/control/sandboxes/${sandboxId}/permission-profile-sources`
22712
24552
  );
22713
24553
  return result.items;
22714
24554
  },
22715
24555
  get: async (sandboxId, profileId) => {
22716
- return this.request(
22717
- `/control/sandboxes/${sandboxId}/permission-profiles/${profileId}`
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
- return this.request(
22722
- `/control/sandboxes/${sandboxId}/permission-profiles`,
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: "POST",
22725
- body: JSON.stringify(data)
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 (sandboxId, profileId) => {
22730
- return this.request(
22731
- `/control/sandboxes/${sandboxId}/permission-profiles/${profileId}`,
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(path6, options = {}) {
22948
- const url = `${this.httpUrl}${path6}`;
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 : ["default"];
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 ?? ["default"]
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 ?? ["default"]
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 permissions = normalizePermissions(options.permissions);
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("--template <template>", "Starter ontology template: library|support|delivery").option("--agent-docs", "Add AGENTS.md and docs/granular-manifest.md (for AI coding agents)").option("--no-agent-docs", "Skip agent documentation files").option("--cursor-hooks", "Emit optional Cursor hook files under .cursor/").option("--cursor-subagents", "Emit optional Cursor subagent files under .cursor/").option("--cursor-all", "Emit both Cursor hooks and Cursor subagents").option("--control-plane-session <sessionId>", "Attach the init flow to a control-plane onboarding session").option("--project-mode <mode>", "Onboarding context: new|existing").option("--auth-strategy <strategy>", "Onboarding context: just-me|granular-auth|existing-auth").option("--ide <ide>", "Onboarding context: cursor|codex|claude-code").option("--data-strategy <strategy>", "Onboarding context: demo|real").option("--product-summary <text>", "Onboarding context: product brief from the control plane").option("--roles-summary <text>", "Onboarding context: roles and permission notes from the control plane").action(async (projectName, opts) => {
23826
- try {
23827
- await initCommand(projectName, {
23828
- skipBuild: opts.skipBuild,
23829
- template: opts.template,
23830
- agentDocs: opts.agentDocs,
23831
- noAgentDocs: opts.noAgentDocs,
23832
- cursorHooks: opts.cursorHooks || opts.cursorAll,
23833
- cursorSubagents: opts.cursorSubagents || opts.cursorAll,
23834
- controlPlaneSession: opts.controlPlaneSession,
23835
- projectMode: opts.projectMode,
23836
- authStrategy: opts.authStrategy,
23837
- ide: opts.ide,
23838
- dataStrategy: opts.dataStrategy,
23839
- productSummary: opts.productSummary,
23840
- rolesSummary: opts.rolesSummary
23841
- });
23842
- } catch (err) {
23843
- error(err.message);
23844
- process.exit(1);
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("--timeout <seconds>", "Browser auth timeout in seconds", (value) => {
23848
- const n = Number.parseInt(value, 10);
23849
- return Number.isFinite(n) ? n : 180;
23850
- }).action(async (opts) => {
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 loginCommand({
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("pull <version-id>").description("Pull the manifest for a specific ontology version").action(async (versionId) => {
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 pullCommand(versionId);
25859
+ await buildCommand();
23865
25860
  } catch (err) {
23866
25861
  error(err.message);
23867
25862
  process.exit(1);
23868
25863
  }
23869
25864
  });
23870
- program2.command("build").description("Create or reuse the ontology version for your current manifest, then run its build").action(async () => {
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 buildCommand();
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("deploy").description("Push the current revision to dev; use --prod to move prod there too").option("--prod", "Also point prod to the current version").action(async (opts) => {
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 deployCommand({ prod: opts.prod });
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(async (versionId, againstVersionId, options) => {
23904
- try {
23905
- await versionDiffCommand(versionId, againstVersionId, options);
23906
- } catch (err) {
23907
- error(err.message);
23908
- process.exit(1);
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("Generate GRANULAR_SANDBOX.md (agent reference) from granular.json").action(async () => {
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("Open the Granular simulator in the browser for the current (or given) ontology").option("--subject-id <subjectId>", "Open the simulator preloaded for a specific subject").option("--environment <environment>", "Add an environment query parameter to the simulator URL").option("--session <sessionId>", "Add a session query parameter to the simulator URL").option("--user <userId>", "Add a user query parameter to the simulator URL").option("--print-url", "Print the simulator URL without opening a browser").action(async (sandboxId, options) => {
23978
- try {
23979
- await simulateCommand(sandboxId, options);
23980
- } catch (err) {
23981
- error(err.message);
23982
- process.exit(1);
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("--ontology <ontologyId>", "Override the ontology id from .granularrc").option("--environment <environment>", "Environment slot to connect to", "dev").option("--user <userId>", "External user id to connect as").option("--permissions <list>", "Comma-separated permission profiles to ensure", "default").option("--json", "Print machine-readable JSON").action(async (options) => {
23987
- try {
23988
- await connectTestCommand(options);
23989
- } catch (err) {
23990
- error(err.message);
23991
- process.exit(1);
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("Inspect live Granular sessions and their Automerge-backed state");
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("--ontology <ontologyId>", "Override the ontology id from .granularrc").option("--environment <environment>", "Environment slot to connect to when no session id is provided", "dev").option("--user <userId>", "External user id to connect as when no session id is provided").option("--permissions <list>", "Comma-separated permission profiles to ensure", "default").option("--session <sessionId>", "Connect to an existing session instead of creating a fresh one").option("--json", "Print machine-readable JSON").action(async (options) => {
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 handler(options);
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("--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>", "List sessions for this exact environment id").option("--status <status>", "Session status filter: active|closed|all", "active").option("--json", "Print machine-readable JSON").action(async (options) => {
24017
- try {
24018
- await sessionListCommand(options);
24019
- } catch (err) {
24020
- error(err.message);
24021
- process.exit(1);
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("Submit a job from a file or inline code and wait for the result").option("--ontology <ontologyId>", "Override the ontology id from .granularrc").option("--environment <environment>", "Environment slot to connect to when no session id is provided", "dev").option("--user <userId>", "External user id to connect as when no session id is provided").option("--permissions <list>", "Comma-separated permission profiles to ensure", "default").option("--session <sessionId>", "Connect to an existing session instead of creating a fresh one").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(async (options) => {
24026
- try {
24027
- await jobRunCommand(options);
24028
- } catch (err) {
24029
- error(err.message);
24030
- process.exit(1);
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("Execute a real GraphQL query against the current Granular environment").requiredOption("--query <query>", "GraphQL query or mutation string").option("--variables <json>", "JSON object of GraphQL variables").option("--ontology <ontologyId>", "Override the ontology id from .granularrc").option("--environment <environment>", "Environment slot to connect to when no session id is provided", "dev").option("--user <userId>", "External user id to connect as when no session id is provided").option("--permissions <list>", "Comma-separated permission profiles to ensure", "default").option("--session <sessionId>", "Connect to an existing session instead of creating a fresh one").option("--json", "Print machine-readable JSON").action(async (options) => {
24034
- try {
24035
- await graphqlCommand(options);
24036
- } catch (err) {
24037
- error(err.message);
24038
- process.exit(1);
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
- ["diff", "Compare declared effects to live ready handlers", effectsDiffCommand]
26193
+ [
26194
+ "diff",
26195
+ "Compare declared effects to live ready handlers",
26196
+ effectsDiffCommand
26197
+ ]
24045
26198
  ]) {
24046
- effects.command(name).description(description).option("--ontology <ontologyId>", "Override the ontology id from .granularrc").option("--environment <environment>", "Environment slot to connect to when no session id is provided", "dev").option("--user <userId>", "External user id to connect as when no session id is provided").option("--permissions <list>", "Comma-separated permission profiles to ensure", "default").option("--session <sessionId>", "Connect to an existing session instead of creating a fresh one").option("--json", "Print machine-readable JSON").action(async (options) => {
24047
- try {
24048
- await handler(options);
24049
- } catch (err) {
24050
- error(err.message);
24051
- process.exit(1);
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);