@granular-software/sdk 0.4.35 → 0.4.37

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() {
@@ -13148,6 +14035,67 @@ external_exports.object({
13148
14035
  transitions: external_exports.array(StateMachineTransitionSchema),
13149
14036
  finalStates: external_exports.array(external_exports.string()).optional()
13150
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()
14098
+ }).strict();
13151
14099
  external_exports.object({
13152
14100
  postCondition: external_exports.union([
13153
14101
  external_exports.string(),
@@ -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,6 +19571,22 @@ 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;
@@ -18358,6 +19603,8 @@ var Session = class {
18358
19603
  lastKnownTools = /* @__PURE__ */ new Map();
18359
19604
  /** Last seen live prompts, keyed by prompt id, for answer normalization */
18360
19605
  promptCache = /* @__PURE__ */ new Map();
19606
+ /** Prompt ids locally answered before the document sync catches up. */
19607
+ hiddenPromptIds = /* @__PURE__ */ new Set();
18361
19608
  constructor(client, clientId) {
18362
19609
  this.client = client;
18363
19610
  this.clientId = clientId || `client_${Date.now()}`;
@@ -18493,8 +19740,9 @@ var Session = class {
18493
19740
  * `effect.invoke` RPC back to the sandbox effect host, where the registered handlers
18494
19741
  * execute locally and return the result to the sandbox.
18495
19742
  */
18496
- async submitJob(code, domainRevision) {
18497
- let revision = domainRevision || this.currentDomainRevision || this.extractDomainRevisionFromDoc(this.client.doc) || void 0;
19743
+ async submitJob(code, domainRevisionOrOptions) {
19744
+ const options = typeof domainRevisionOrOptions === "string" ? { domainRevision: domainRevisionOrOptions } : domainRevisionOrOptions || {};
19745
+ let revision = options.domainRevision || this.currentDomainRevision || this.extractDomainRevisionFromDoc(this.client.doc) || void 0;
18498
19746
  if (!revision) {
18499
19747
  try {
18500
19748
  const summary = await this.getDomain();
@@ -18509,7 +19757,9 @@ var Session = class {
18509
19757
  }
18510
19758
  const result = await this.client.call("job.submit", {
18511
19759
  domainRevision: revision,
18512
- code
19760
+ code,
19761
+ metadata: options.metadata,
19762
+ agent: options.agent
18513
19763
  });
18514
19764
  if (!result.jobId) {
18515
19765
  throw new Error("Failed to submit job: no jobId returned");
@@ -18550,25 +19800,39 @@ var Session = class {
18550
19800
  const prompt3 = this.promptCache.get(promptId);
18551
19801
  const resolvedAnswer = resolvePromptAnswer(prompt3, answer);
18552
19802
  this.promptCache.delete(promptId);
18553
- await this.client.call("prompt.answer", {
18554
- promptId,
18555
- answer: resolvedAnswer,
18556
- value: resolvedAnswer
18557
- });
19803
+ this.hiddenPromptIds.add(promptId);
19804
+ try {
19805
+ await this.client.call("prompt.answer", {
19806
+ promptId,
19807
+ answer: resolvedAnswer,
19808
+ value: resolvedAnswer
19809
+ });
19810
+ } catch (error2) {
19811
+ this.hiddenPromptIds.delete(promptId);
19812
+ if (prompt3) {
19813
+ this.promptCache.set(promptId, prompt3);
19814
+ }
19815
+ throw error2;
19816
+ }
18558
19817
  try {
18559
19818
  const content = this.stringifyConversationValue(resolvedAnswer);
18560
19819
  if (content.trim()) {
18561
- await this.appendConversationMessage({
18562
- role: "user",
18563
- content,
18564
- promptId
18565
- });
19820
+ await withPromptTranscriptTimeout(
19821
+ this.appendConversationMessage({
19822
+ role: "user",
19823
+ content,
19824
+ promptId
19825
+ })
19826
+ );
18566
19827
  }
18567
19828
  } catch {
18568
19829
  }
18569
19830
  }
18570
19831
  async appendConversationMessage(input) {
18571
- return this.client.call("conversation.append", input);
19832
+ return this.client.call(
19833
+ "conversation.append",
19834
+ input
19835
+ );
18572
19836
  }
18573
19837
  /**
18574
19838
  * Get the current list of available effects.
@@ -18577,9 +19841,53 @@ var Session = class {
18577
19841
  getEffects() {
18578
19842
  const doc = this.client.doc;
18579
19843
  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) {
19844
+ const domainPackages = doc.domain?.packages;
19845
+ const packageCandidates = domainPackages && typeof domainPackages === "object" ? [
19846
+ domainPackages.domain,
19847
+ domainPackages["@sandbox/domain"],
19848
+ ...Object.values(domainPackages)
19849
+ ].filter(Boolean) : [];
19850
+ for (const domainPkg of packageCandidates) {
19851
+ if (domainPkg?.tools && Array.isArray(domainPkg.tools)) {
19852
+ for (const tool of domainPkg.tools) {
19853
+ if (!tool?.name || toolMap.has(tool.name)) continue;
19854
+ toolMap.set(tool.name, {
19855
+ name: tool.name,
19856
+ description: tool.description,
19857
+ inputSchema: tool.inputSchema,
19858
+ outputSchema: tool.outputSchema,
19859
+ className: tool.className || void 0,
19860
+ static: tool.static || false,
19861
+ ready: false,
19862
+ publishedAt: void 0
19863
+ });
19864
+ }
19865
+ }
19866
+ if (!domainPkg?.classes || typeof domainPkg.classes !== "object") {
19867
+ continue;
19868
+ }
19869
+ for (const [className, classDef] of Object.entries(
19870
+ domainPkg.classes
19871
+ )) {
19872
+ const methods = Array.isArray(classDef?.methods) ? classDef.methods : [];
19873
+ for (const method of methods) {
19874
+ if (!method?.name || toolMap.has(method.name)) continue;
19875
+ toolMap.set(method.name, {
19876
+ name: method.name,
19877
+ description: method.description,
19878
+ inputSchema: method.inputSchema,
19879
+ outputSchema: method.outputSchema,
19880
+ className: method.className || classDef?.name || className,
19881
+ static: method.static || false,
19882
+ ready: false,
19883
+ publishedAt: void 0
19884
+ });
19885
+ }
19886
+ }
19887
+ }
19888
+ const legacyDomainPkg = doc.domain?.packages?.domain;
19889
+ if (legacyDomainPkg?.tools && Array.isArray(legacyDomainPkg.tools)) {
19890
+ for (const tool of legacyDomainPkg.tools) {
18583
19891
  if (!tool?.name) continue;
18584
19892
  toolMap.set(tool.name, {
18585
19893
  name: tool.name,
@@ -18593,6 +19901,27 @@ var Session = class {
18593
19901
  });
18594
19902
  }
18595
19903
  }
19904
+ if (legacyDomainPkg?.classes && typeof legacyDomainPkg.classes === "object") {
19905
+ for (const [className, classDef] of Object.entries(
19906
+ legacyDomainPkg.classes
19907
+ )) {
19908
+ const methods = Array.isArray(classDef?.methods) ? classDef.methods : [];
19909
+ for (const method of methods) {
19910
+ if (!method?.name || toolMap.has(method.name)) continue;
19911
+ toolMap.set(method.name, {
19912
+ name: method.name,
19913
+ description: method.description,
19914
+ inputSchema: method.inputSchema,
19915
+ outputSchema: method.outputSchema,
19916
+ className: method.className || classDef?.name || className,
19917
+ static: method.static || false,
19918
+ ready: false,
19919
+ publishedAt: void 0
19920
+ });
19921
+ }
19922
+ }
19923
+ }
19924
+ const hasPolicyFilteredDomainTools = toolMap.size > 0;
18596
19925
  const catalogs = doc.catalog?.rawToolCatalogs || {};
18597
19926
  for (const [clientId, catalog] of Object.entries(catalogs)) {
18598
19927
  const cat = catalog;
@@ -18600,6 +19929,7 @@ var Session = class {
18600
19929
  for (const tool of cat.tools) {
18601
19930
  if (!tool?.name) continue;
18602
19931
  const existing = toolMap.get(tool.name);
19932
+ if (hasPolicyFilteredDomainTools && !existing) continue;
18603
19933
  if (existing?.publishedAt && cat.publishedAt && existing.publishedAt > cat.publishedAt)
18604
19934
  continue;
18605
19935
  const isLocal = clientId === this.clientId;
@@ -18619,6 +19949,24 @@ var Session = class {
18619
19949
  }
18620
19950
  return Array.from(toolMap.values());
18621
19951
  }
19952
+ /**
19953
+ * Return the currently open prompt payloads known to this session.
19954
+ *
19955
+ * These come from live `prompt` / `prompt.request` websocket events and
19956
+ * preserve the exact shape used by `answerPrompt(...)`.
19957
+ */
19958
+ getPrompts() {
19959
+ return Array.from(this.promptCache.values()).map((prompt3) => ({
19960
+ ...prompt3,
19961
+ options: Array.isArray(prompt3.options) ? prompt3.options.map(
19962
+ (option) => typeof option === "string" ? option : { ...option }
19963
+ ) : void 0,
19964
+ metadata: prompt3.metadata ? { ...prompt3.metadata } : void 0
19965
+ }));
19966
+ }
19967
+ getHiddenPromptIds() {
19968
+ return Array.from(this.hiddenPromptIds);
19969
+ }
18622
19970
  /**
18623
19971
  * Backwards-compatible alias for `getEffects()`.
18624
19972
  */
@@ -18718,11 +20066,7 @@ var Session = class {
18718
20066
  if (!normalizedDocs) {
18719
20067
  return normalizedTypes;
18720
20068
  }
18721
- return [
18722
- normalizedTypes,
18723
- "Generated usage notes from ./sandbox-tools docs:",
18724
- normalizedDocs
18725
- ].join("\n\n");
20069
+ return [normalizedTypes, "[Docs]", normalizedDocs].join("\n\n");
18726
20070
  }
18727
20071
  if (normalizedDocs) {
18728
20072
  return normalizedDocs;
@@ -18944,6 +20288,7 @@ import { ${allImports} } from "./sandbox-tools";
18944
20288
  const emitPrompt = (payload) => {
18945
20289
  const prompt3 = normalizePrompt(payload);
18946
20290
  if (!prompt3) return;
20291
+ this.hiddenPromptIds.delete(prompt3.id);
18947
20292
  this.promptCache.set(prompt3.id, prompt3);
18948
20293
  this.emit("prompt", prompt3);
18949
20294
  };
@@ -19126,6 +20471,7 @@ var JobImplementation = class {
19126
20471
  eventListeners = /* @__PURE__ */ new Map();
19127
20472
  bufferedAgentMessages = [];
19128
20473
  bufferedAgentMessageIds = /* @__PURE__ */ new Set();
20474
+ resultSettled = false;
19129
20475
  metadata;
19130
20476
  constructor(id, client, initialState) {
19131
20477
  this.id = id;
@@ -19150,7 +20496,9 @@ var JobImplementation = class {
19150
20496
  if (execData.error) {
19151
20497
  this.finalize("failed", void 0, execData.error);
19152
20498
  } else {
19153
- this.finalize("succeeded", execData.result);
20499
+ this.finalize("succeeded", execData.result, void 0, {
20500
+ hasResult: Object.prototype.hasOwnProperty.call(execData, "result")
20501
+ });
19154
20502
  }
19155
20503
  this.emit("status", this.status);
19156
20504
  }
@@ -19186,9 +20534,6 @@ var JobImplementation = class {
19186
20534
  if (normalizedStatus === "failed" || normalizedStatus === "timeout" || normalizedStatus === "canceled") {
19187
20535
  this.finalize(normalizedStatus);
19188
20536
  }
19189
- if (normalizedStatus === "succeeded") {
19190
- this.finalize("succeeded");
19191
- }
19192
20537
  this.emit("status", normalizedStatus);
19193
20538
  });
19194
20539
  this.client.on(`job.${id}.stdout`, (line) => {
@@ -19208,7 +20553,7 @@ var JobImplementation = class {
19208
20553
  this.emit("stderr", line);
19209
20554
  });
19210
20555
  this.client.on(`job.${id}.result`, (result) => {
19211
- this.finalize("succeeded", result);
20556
+ this.finalize("succeeded", result, void 0, { hasResult: true });
19212
20557
  });
19213
20558
  this.client.on(`job.${id}.error`, (error2) => {
19214
20559
  this.finalize("failed", void 0, error2);
@@ -19229,7 +20574,9 @@ var JobImplementation = class {
19229
20574
  this.client.on("job.completed", (data) => {
19230
20575
  const jobData = data;
19231
20576
  if (jobData.jobId === id) {
19232
- this.finalize("succeeded", jobData.result);
20577
+ this.finalize("succeeded", jobData.result, void 0, {
20578
+ hasResult: true
20579
+ });
19233
20580
  this.emit("status", this.status);
19234
20581
  }
19235
20582
  });
@@ -19354,7 +20701,7 @@ var JobImplementation = class {
19354
20701
  this.metadata.status = "running";
19355
20702
  }
19356
20703
  }
19357
- finalize(status, result, error2) {
20704
+ finalize(status, result, error2, options = {}) {
19358
20705
  if (!this.metadata.startedAt) {
19359
20706
  this.metadata.startedAt = Date.now();
19360
20707
  }
@@ -19362,14 +20709,18 @@ var JobImplementation = class {
19362
20709
  this.metadata.status = status;
19363
20710
  this.metadata.completedAt = this.metadata.completedAt || Date.now();
19364
20711
  this.metadata.durationMs = this.metadata.completedAt - this.metadata.startedAt;
19365
- if (result !== void 0) {
20712
+ if (!this.resultSettled && (options.hasResult || result !== void 0)) {
19366
20713
  this.metadata.result = sanitizeFeedbackValue(result);
20714
+ this.resultSettled = true;
19367
20715
  this._resolveResult(result);
19368
20716
  }
19369
- if (error2 !== void 0) {
19370
- const message = error2 instanceof Error ? error2.message : String(error2);
20717
+ if (!this.resultSettled && (error2 !== void 0 || status === "failed" || status === "timeout" || status === "canceled")) {
20718
+ const fallbackError = new Error(`Job ${this.id} ${status}.`);
20719
+ const cause = error2 ?? fallbackError;
20720
+ const message = cause instanceof Error ? cause.message : String(cause);
19371
20721
  this.metadata.error = truncateFeedbackString(message);
19372
- this._rejectResult(error2);
20722
+ this.resultSettled = true;
20723
+ this._rejectResult(cause);
19373
20724
  }
19374
20725
  }
19375
20726
  upsertToolCall(next) {
@@ -19452,6 +20803,17 @@ function humanTextFromStdout(stdout) {
19452
20803
  }
19453
20804
  return null;
19454
20805
  }
20806
+ function responseTextFromAgentMessages(agentMessages) {
20807
+ for (const message of [...agentMessages].reverse()) {
20808
+ const record = asRecord2(message);
20809
+ if (!record) continue;
20810
+ for (const key of RESPONSE_KEYS) {
20811
+ const normalized = normalizeText(record[key]);
20812
+ if (normalized) return normalized;
20813
+ }
20814
+ }
20815
+ return null;
20816
+ }
19455
20817
  function pushString(target, value) {
19456
20818
  if (typeof value === "string" && value.trim()) {
19457
20819
  target.add(value.trim());
@@ -19477,6 +20839,41 @@ function collectReferencesFromRecord(record, refs) {
19477
20839
  for (const key of VARIABLE_ARRAY_KEY_CANDIDATES)
19478
20840
  pushStringArray(refs.variableNames, record[key]);
19479
20841
  }
20842
+ function stringValue(record, keys) {
20843
+ for (const key of keys) {
20844
+ const value = record[key];
20845
+ if (typeof value === "string" && value.trim()) {
20846
+ return value.trim();
20847
+ }
20848
+ }
20849
+ return null;
20850
+ }
20851
+ function findEntryPathForRecord(record, heap) {
20852
+ const directPath = stringValue(record, ["entryPath", "path"]);
20853
+ if (directPath && heap.entriesByPath?.[directPath]) {
20854
+ return directPath;
20855
+ }
20856
+ const id = stringValue(record, ["id", "_id", "recordId", "objectId"]);
20857
+ if (!id) {
20858
+ return null;
20859
+ }
20860
+ const className = stringValue(record, [
20861
+ "className",
20862
+ "_className",
20863
+ "__className",
20864
+ "prototype",
20865
+ "type"
20866
+ ]);
20867
+ const entries = Object.values(heap.entriesByPath || {});
20868
+ const exact = entries.find(
20869
+ (entry) => entry.id === id && (!className || entry.className === className || entry.prototypes?.includes(className))
20870
+ );
20871
+ if (exact?.path) {
20872
+ return exact.path;
20873
+ }
20874
+ const idOnlyMatches = entries.filter((entry) => entry.id === id);
20875
+ return idOnlyMatches.length === 1 ? idOnlyMatches[0].path : null;
20876
+ }
19480
20877
  function scanForHeapReferences(value, heap, refs, depth = 0, seen = /* @__PURE__ */ new Set()) {
19481
20878
  if (value === null || value === void 0 || depth > 4 || seen.has(value))
19482
20879
  return;
@@ -19497,6 +20894,8 @@ function scanForHeapReferences(value, heap, refs, depth = 0, seen = /* @__PURE__
19497
20894
  const record = asRecord2(value);
19498
20895
  if (!record) return;
19499
20896
  seen.add(value);
20897
+ const entryPath = findEntryPathForRecord(record, heap);
20898
+ if (entryPath) refs.entryPaths.add(entryPath);
19500
20899
  collectReferencesFromRecord(record, refs);
19501
20900
  for (const key of UI_CONTAINER_KEYS) {
19502
20901
  const nested = asRecord2(record[key]);
@@ -19594,8 +20993,8 @@ function getJobRelatedLists(heap, jobId) {
19594
20993
  function entriesFromLists(lists, heap) {
19595
20994
  const entries = [];
19596
20995
  for (const list of lists) {
19597
- for (const path6 of list.paths || []) {
19598
- const entry = heap.entriesByPath?.[path6];
20996
+ for (const path7 of list.paths || []) {
20997
+ const entry = heap.entriesByPath?.[path7];
19599
20998
  if (entry) entries.push(entry);
19600
20999
  }
19601
21000
  }
@@ -19605,6 +21004,7 @@ function resolveJobPresentation({
19605
21004
  jobId,
19606
21005
  result,
19607
21006
  stdout = [],
21007
+ agentMessages = [],
19608
21008
  sessionHeap,
19609
21009
  allowExplicitArtifacts = true
19610
21010
  }) {
@@ -19621,7 +21021,7 @@ function resolveJobPresentation({
19621
21021
  [...refs.listNames].map((name) => sessionHeap.listsByName?.[name]).filter((list) => Boolean(list))
19622
21022
  );
19623
21023
  const referencedEntries = sortEntries(
19624
- [...refs.entryPaths].map((path6) => sessionHeap.entriesByPath?.[path6]).filter((entry) => Boolean(entry))
21024
+ [...refs.entryPaths].map((path7) => sessionHeap.entriesByPath?.[path7]).filter((entry) => Boolean(entry))
19625
21025
  );
19626
21026
  const jobLists = getJobRelatedLists(sessionHeap, jobId);
19627
21027
  const jobEntries = getJobRelatedEntries(sessionHeap, jobId);
@@ -19637,7 +21037,7 @@ function resolveJobPresentation({
19637
21037
  const hasExplicitArtifacts = allowExplicitArtifacts && (explicitEntries.length > 0 || explicitLists.length > 0);
19638
21038
  const lists = hasExplicitArtifacts ? explicitLists : jobLists;
19639
21039
  const entries = hasExplicitArtifacts ? explicitEntries : changedEntries;
19640
- const responseText = extractResponseText(result, stdout) || fallbackResponseText(entries, lists);
21040
+ const responseText = extractResponseText(result, stdout) || responseTextFromAgentMessages(agentMessages) || fallbackResponseText(entries, lists);
19641
21041
  return {
19642
21042
  responseText,
19643
21043
  entries,
@@ -20172,6 +21572,12 @@ var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT = 3;
20172
21572
  var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_DELAY_MS = 1e3;
20173
21573
  var LOCAL_CONTROL_REQUEST_RETRY_COUNT = 4;
20174
21574
  var LOCAL_CONTROL_REQUEST_RETRY_DELAY_MS = 500;
21575
+ var SESSION_DATA_REQUEST_RETRY_COUNT = 4;
21576
+ var SESSION_DATA_REQUEST_RETRY_DELAY_MS = 500;
21577
+ var EFFECT_HOST_CONNECT_TIMEOUT_MS = 15e3;
21578
+ var EFFECT_CATALOG_SYNC_TIMEOUT_MS = 3e4;
21579
+ var EFFECT_CATALOG_SYNC_RETRY_COUNT = 3;
21580
+ var EFFECT_CATALOG_SYNC_RETRY_DELAY_MS = 1e3;
20175
21581
  function planRecordObjectsChunks(records, batchSize) {
20176
21582
  const total = records.length;
20177
21583
  const size = Math.max(1, Math.min(batchSize, total));
@@ -20186,6 +21592,19 @@ function planRecordObjectsChunks(records, batchSize) {
20186
21592
  function sleep(ms) {
20187
21593
  return new Promise((resolve2) => setTimeout(resolve2, ms));
20188
21594
  }
21595
+ function withTimeout(promise, timeoutMs, label) {
21596
+ let timer = null;
21597
+ const timeout = new Promise((_, reject) => {
21598
+ timer = setTimeout(() => {
21599
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`));
21600
+ }, timeoutMs);
21601
+ });
21602
+ return Promise.race([promise, timeout]).finally(() => {
21603
+ if (timer) {
21604
+ clearTimeout(timer);
21605
+ }
21606
+ });
21607
+ }
20189
21608
  function isLocalControlUrl(url) {
20190
21609
  try {
20191
21610
  const parsed = new URL(url);
@@ -20199,7 +21618,19 @@ function isRetryableLocalWorkerRestart(status, body, url) {
20199
21618
  }
20200
21619
  function isRetryableRecordObjectsError(error2) {
20201
21620
  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(
21621
+ 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(
21622
+ message
21623
+ );
21624
+ }
21625
+ function isRetryableEffectRegistrationError(error2) {
21626
+ const message = error2 instanceof Error ? error2.message : String(error2);
21627
+ 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(
21628
+ message
21629
+ );
21630
+ }
21631
+ function isRetryableSessionDataError(error2) {
21632
+ const message = error2 instanceof Error ? error2.message : String(error2);
21633
+ 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
21634
  message
20204
21635
  );
20205
21636
  }
@@ -20221,16 +21652,28 @@ function computeEffectRegistrationKey(effect) {
20221
21652
  effect.versionSelector
20222
21653
  )}`;
20223
21654
  }
20224
- function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId) {
20225
- const url = new URL(apiUrl);
20226
- if (url.pathname.endsWith("/granular/ws/connect")) {
21655
+ function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId, effectHostUrl) {
21656
+ const overrideUrl = effectHostUrl || process.env.GRANULAR_EFFECT_HOST_URL || process.env.EFFECT_HOST_URL;
21657
+ const api = new URL(apiUrl);
21658
+ const localRuntimeBase = process.env.RUNTIME_ORCHESTRATOR_URL || (isLocalControlUrl(apiUrl) ? `${api.protocol}//${api.hostname}:8791` : "");
21659
+ const url = new URL(overrideUrl || localRuntimeBase || apiUrl);
21660
+ if (url.protocol === "https:") {
21661
+ url.protocol = "wss:";
21662
+ } else if (url.protocol === "http:") {
21663
+ url.protocol = "ws:";
21664
+ }
21665
+ if (!overrideUrl && isLocalControlUrl(apiUrl) && api.pathname.endsWith("/granular")) {
21666
+ url.pathname = "/granular/orchestrator/effects/connect";
21667
+ } else if (url.pathname.endsWith("/granular/ws/connect")) {
20227
21668
  url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
20228
21669
  } else if (url.pathname.endsWith("/granular")) {
20229
- url.pathname = `${url.pathname.replace(/\/$/, "")}/effects/connect`;
21670
+ url.pathname = isLocalControlUrl(url.toString()) ? "/granular/orchestrator/effects/connect" : `${url.pathname.replace(/\/$/, "")}/effects/connect`;
20230
21671
  } else if (url.pathname.endsWith("/v2/ws/connect")) {
20231
21672
  url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
20232
21673
  } else if (url.pathname.endsWith("/v2/ws")) {
20233
21674
  url.pathname = url.pathname.replace(/\/ws$/, "/effects/connect");
21675
+ } else if (url.pathname === "/" && isLocalControlUrl(url.toString()) && (url.port === "8791" || !overrideUrl && Boolean(localRuntimeBase))) {
21676
+ url.pathname = "/granular/orchestrator/effects/connect";
20234
21677
  } else if (url.pathname.endsWith("/ws/connect")) {
20235
21678
  url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
20236
21679
  } else if (url.pathname.endsWith("/ws")) {
@@ -20265,6 +21708,79 @@ function normalizeHeapSnapshot(raw) {
20265
21708
  updatedAt: typeof heap.updatedAt === "number" ? heap.updatedAt : Date.now()
20266
21709
  };
20267
21710
  }
21711
+ function normalizeGraphPathSegment(value) {
21712
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
21713
+ }
21714
+ function extractRecordIdFromGraphPath(path7, className) {
21715
+ const normalizedPrefix = `${normalizeGraphPathSegment(className)}_`;
21716
+ if (path7.startsWith(normalizedPrefix)) {
21717
+ return path7.slice(normalizedPrefix.length);
21718
+ }
21719
+ const legacyPrefix = `${className}_`;
21720
+ if (path7.startsWith(legacyPrefix)) {
21721
+ return path7.slice(legacyPrefix.length);
21722
+ }
21723
+ return path7;
21724
+ }
21725
+ function toRecordSearchResult(className, node) {
21726
+ const path7 = typeof node.path === "string" ? node.path : "";
21727
+ if (!path7) return null;
21728
+ const fields = Array.isArray(node.submodels) ? node.submodels.flatMap(
21729
+ (submodel) => {
21730
+ const name = typeof submodel?.label === "string" && submodel.label.trim() ? submodel.label : typeof submodel?.path === "string" ? submodel.path.split(":").pop() || submodel.path : "";
21731
+ if (!name) return [];
21732
+ if (typeof submodel.string_value === "string") {
21733
+ return [{ name, type: "string", value: submodel.string_value }];
21734
+ }
21735
+ if (typeof submodel.number_value === "number") {
21736
+ return [{ name, type: "number", value: submodel.number_value }];
21737
+ }
21738
+ if (typeof submodel.boolean_value === "boolean") {
21739
+ return [
21740
+ {
21741
+ name,
21742
+ type: "boolean",
21743
+ value: submodel.boolean_value
21744
+ }
21745
+ ];
21746
+ }
21747
+ return [];
21748
+ }
21749
+ ) : [];
21750
+ return {
21751
+ path: path7,
21752
+ className,
21753
+ id: extractRecordIdFromGraphPath(path7, className),
21754
+ label: typeof node.label === "string" && node.label.trim() ? node.label : extractRecordIdFromGraphPath(path7, className),
21755
+ description: typeof node.description === "string" && node.description.trim() ? node.description : null,
21756
+ fields
21757
+ };
21758
+ }
21759
+ function normalizeRecordSearchText(value) {
21760
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
21761
+ }
21762
+ function rankRecordSearchResult(result, query, index) {
21763
+ const normalizedQuery = normalizeRecordSearchText(query);
21764
+ if (!normalizedQuery) {
21765
+ return index;
21766
+ }
21767
+ const label = normalizeRecordSearchText(result.label || "");
21768
+ const id = normalizeRecordSearchText(result.id || "");
21769
+ const path7 = normalizeRecordSearchText(result.path || "");
21770
+ const className = normalizeRecordSearchText(result.className || "");
21771
+ const searchable = [label, id, path7, className].filter(Boolean);
21772
+ if (label === normalizedQuery) return index;
21773
+ if (id === normalizedQuery || path7 === normalizedQuery) return 100 + index;
21774
+ if (label.startsWith(normalizedQuery)) return 200 + index;
21775
+ if (searchable.some((value) => value.startsWith(normalizedQuery))) {
21776
+ return 300 + index;
21777
+ }
21778
+ if (label.includes(normalizedQuery)) return 400 + index;
21779
+ if (searchable.some((value) => value.includes(normalizedQuery))) {
21780
+ return 500 + index;
21781
+ }
21782
+ return 900 + index;
21783
+ }
20268
21784
  function deriveRuntimeBaseUrl(apiEndpoint) {
20269
21785
  try {
20270
21786
  const endpoint = new URL(apiEndpoint);
@@ -20353,7 +21869,7 @@ function normalizeEnvironmentData(environment) {
20353
21869
  setup: normalizeEnvironmentSetupSummary(environment.setup)
20354
21870
  };
20355
21871
  }
20356
- var Environment = class {
21872
+ var Environment = class _Environment {
20357
21873
  granular;
20358
21874
  envData;
20359
21875
  _apiKey;
@@ -20528,9 +22044,9 @@ var Environment = class {
20528
22044
  getRuntimeBaseUrl() {
20529
22045
  return deriveRuntimeBaseUrl(this._apiEndpoint);
20530
22046
  }
20531
- async controlPlaneRequest(path6, options = {}) {
22047
+ async controlPlaneRequest(path7, options = {}) {
20532
22048
  const runtimeBase = this.getRuntimeBaseUrl();
20533
- const url = `${runtimeBase}${path6}`;
22049
+ const url = `${runtimeBase}${path7}`;
20534
22050
  const response = await fetch(url, {
20535
22051
  ...options,
20536
22052
  headers: {
@@ -20548,28 +22064,30 @@ var Environment = class {
20548
22064
  return response.json();
20549
22065
  }
20550
22066
  // ==================== ID ↔ GRAPH PATH MAPPING ====================
22067
+ static normalizeGraphPathSegment(value) {
22068
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
22069
+ }
20551
22070
  /**
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.
20556
- *
20557
- * Format: `{className}_{id}` — deterministic, human-readable.
22071
+ * Convert a class name + application record ID into Granular's graph path.
20558
22072
  *
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`.
22073
+ * This mirrors the record-write path normalization used by the control plane.
22074
+ * Keep the original customer/system ID in `real_id`; graph paths are stable
22075
+ * internal addresses, not the source of truth for business identity.
20562
22076
  */
20563
22077
  static toGraphPath(className, id) {
20564
- return `${className}_${id}`;
22078
+ return `${_Environment.normalizeGraphPathSegment(className)}_${_Environment.normalizeGraphPathSegment(id)}`;
20565
22079
  }
20566
22080
  /**
20567
- * Extract the real-world ID from a graph path, given the class name.
22081
+ * Best-effort extraction of an ID-like suffix from a graph path.
20568
22082
  *
20569
- * Strips the `{className}_` prefix. Returns the raw path if the
20570
- * expected prefix is not found.
22083
+ * Prefer the record's `real_id` field whenever exact customer/system IDs
22084
+ * matter, because graph path normalization is intentionally lossy.
20571
22085
  */
20572
22086
  static extractIdFromGraphPath(graphPath, className) {
22087
+ const normalizedPrefix = `${_Environment.normalizeGraphPathSegment(className)}_`;
22088
+ if (graphPath.startsWith(normalizedPrefix)) {
22089
+ return graphPath.substring(normalizedPrefix.length);
22090
+ }
20573
22091
  const prefix = `${className}_`;
20574
22092
  return graphPath.startsWith(prefix) ? graphPath.substring(prefix.length) : graphPath;
20575
22093
  }
@@ -20616,6 +22134,62 @@ var Environment = class {
20616
22134
  }
20617
22135
  return response.json();
20618
22136
  }
22137
+ async searchRecords(query, options = {}) {
22138
+ const normalizedQuery = query.replace(/\s+/g, " ").trim();
22139
+ const limit = Math.max(1, Math.min(50, Math.floor(options.limit ?? 12)));
22140
+ const offset = Math.max(0, Math.floor(options.offset ?? 0));
22141
+ const response = await this.graphql(
22142
+ `
22143
+ query RecordMentionSearch(
22144
+ $query: String
22145
+ $limit: Int
22146
+ $offset: Int
22147
+ $classNames: [String!]
22148
+ ) {
22149
+ record_search(
22150
+ query: $query
22151
+ limit: $limit
22152
+ offset: $offset
22153
+ class_names: $classNames
22154
+ ) {
22155
+ className
22156
+ model {
22157
+ path
22158
+ label
22159
+ description
22160
+ submodels {
22161
+ path
22162
+ label
22163
+ string_value
22164
+ number_value
22165
+ boolean_value
22166
+ }
22167
+ }
22168
+ }
22169
+ }
22170
+ `,
22171
+ {
22172
+ query: normalizedQuery,
22173
+ limit,
22174
+ offset,
22175
+ classNames: options.classNames?.length ? options.classNames : []
22176
+ }
22177
+ );
22178
+ const seen = /* @__PURE__ */ new Set();
22179
+ const results = (response.data?.record_search || []).flatMap((entry) => {
22180
+ const className = entry.className?.trim();
22181
+ const item = className && entry.model ? toRecordSearchResult(className, entry.model) : null;
22182
+ if (!item || seen.has(item.path)) {
22183
+ return [];
22184
+ }
22185
+ seen.add(item.path);
22186
+ return [item];
22187
+ });
22188
+ return results.map((result, index) => ({
22189
+ result,
22190
+ rank: rankRecordSearchResult(result, normalizedQuery, index)
22191
+ })).sort((left, right) => left.rank - right.rank).map((item) => item.result).slice(0, limit);
22192
+ }
20619
22193
  // ==================== RELATIONSHIP METHODS ====================
20620
22194
  /**
20621
22195
  * Define a relationship between two model types.
@@ -21381,7 +22955,8 @@ var Environment = class {
21381
22955
  body: JSON.stringify({
21382
22956
  records,
21383
22957
  batchSize: options.batchSize,
21384
- setupRunId: options.setupRunId
22958
+ setupRunId: options.setupRunId,
22959
+ writeMode: options.writeMode
21385
22960
  })
21386
22961
  }
21387
22962
  );
@@ -21433,11 +23008,13 @@ var Environment = class {
21433
23008
  };
21434
23009
  var EnvironmentSession = class extends Session {
21435
23010
  environment;
23011
+ sessionDataRoutePrefix;
21436
23012
  /** The last known graph container status, updated by checkReadiness() or on heartbeat */
21437
23013
  graphContainerStatus = null;
21438
- constructor(client, environment, clientId) {
23014
+ constructor(client, environment, clientId, options = {}) {
21439
23015
  super(client, clientId);
21440
23016
  this.environment = environment;
23017
+ this.sessionDataRoutePrefix = options.sessionDataRoutePrefix || "/orchestrator/ws/sessions";
21441
23018
  }
21442
23019
  get environmentId() {
21443
23020
  return this.environment.environmentId;
@@ -21482,7 +23059,7 @@ var EnvironmentSession = class extends Session {
21482
23059
  const doc = this.document;
21483
23060
  return normalizeHeapSnapshot(doc?.heap);
21484
23061
  }
21485
- async sessionDataRequest(path6, query) {
23062
+ async sessionDataRequest(path7, query, init2 = {}) {
21486
23063
  const searchParams = new URLSearchParams();
21487
23064
  for (const [key, value] of Object.entries(query || {})) {
21488
23065
  if (value !== null && typeof value !== "undefined" && value !== "") {
@@ -21490,23 +23067,39 @@ var EnvironmentSession = class extends Session {
21490
23067
  }
21491
23068
  }
21492
23069
  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"
23070
+ const url = `${this.environment.runtimeBaseUrl}${this.sessionDataRoutePrefix}/${encodeURIComponent(this.sessionId)}${path7}${queryString ? `?${queryString}` : ""}`;
23071
+ const body = typeof init2.body === "undefined" ? void 0 : JSON.stringify(init2.body);
23072
+ for (let attempt = 1; attempt <= SESSION_DATA_REQUEST_RETRY_COUNT; attempt += 1) {
23073
+ try {
23074
+ const response = await fetch(url, {
23075
+ method: init2.method || "GET",
23076
+ headers: {
23077
+ Authorization: `Bearer ${this.environment.authToken}`,
23078
+ "Content-Type": "application/json"
23079
+ },
23080
+ ...typeof body === "undefined" ? {} : { body }
23081
+ });
23082
+ if (response.ok) {
23083
+ return response.json();
23084
+ }
23085
+ const errorText = await response.text();
23086
+ const error2 = new Error(
23087
+ `Session data API Error (${response.status}): ${errorText}`
23088
+ );
23089
+ if (isLocalControlUrl(url) && isRetryableSessionDataError(error2) && attempt < SESSION_DATA_REQUEST_RETRY_COUNT) {
23090
+ await sleep(SESSION_DATA_REQUEST_RETRY_DELAY_MS * attempt);
23091
+ continue;
23092
+ }
23093
+ throw error2;
23094
+ } catch (error2) {
23095
+ if (isLocalControlUrl(url) && isRetryableSessionDataError(error2) && attempt < SESSION_DATA_REQUEST_RETRY_COUNT) {
23096
+ await sleep(SESSION_DATA_REQUEST_RETRY_DELAY_MS * attempt);
23097
+ continue;
21500
23098
  }
23099
+ throw error2;
21501
23100
  }
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
23101
  }
21509
- return response.json();
23102
+ throw new Error(`Session data API Error: exhausted retries for ${url}`);
21510
23103
  }
21511
23104
  async collectAllSessionItems(listPage) {
21512
23105
  const items = [];
@@ -21552,8 +23145,8 @@ var EnvironmentSession = class extends Session {
21552
23145
  return {
21553
23146
  entries: {
21554
23147
  list: (options = {}) => this.sessionDataRequest("/heap/entries", options),
21555
- get: (path6) => this.sessionDataRequest(
21556
- `/heap/entries/${encodeURIComponent(path6)}`
23148
+ get: (path7) => this.sessionDataRequest(
23149
+ `/heap/entries/${encodeURIComponent(path7)}`
21557
23150
  )
21558
23151
  },
21559
23152
  lists: {
@@ -21564,6 +23157,17 @@ var EnvironmentSession = class extends Session {
21564
23157
  get: (name) => this.sessionDataRequest(
21565
23158
  `/heap/lists/${encodeURIComponent(name)}`
21566
23159
  )
23160
+ },
23161
+ variables: {
23162
+ list: (options = {}) => this.sessionDataRequest("/heap/variables", options),
23163
+ get: (name) => this.sessionDataRequest(
23164
+ `/heap/variables/${encodeURIComponent(name)}`
23165
+ ),
23166
+ delete: (name) => this.sessionDataRequest(
23167
+ `/heap/variables/${encodeURIComponent(name)}`,
23168
+ void 0,
23169
+ { method: "DELETE" }
23170
+ )
21567
23171
  }
21568
23172
  };
21569
23173
  }
@@ -21632,6 +23236,19 @@ var EnvironmentSession = class extends Session {
21632
23236
  async graphql(query, variables) {
21633
23237
  return this.environment.graphql(query, variables);
21634
23238
  }
23239
+ async searchRecords(query, options = {}) {
23240
+ return this.environment.searchRecords(query, options);
23241
+ }
23242
+ async mentionRecord(input) {
23243
+ return this.sessionDataRequest(
23244
+ "/records/mention",
23245
+ void 0,
23246
+ {
23247
+ method: "POST",
23248
+ body: input
23249
+ }
23250
+ );
23251
+ }
21635
23252
  async defineRelationship(options) {
21636
23253
  return this.environment.defineRelationship(options);
21637
23254
  }
@@ -21779,6 +23396,7 @@ var Granular = class _Granular {
21779
23396
  WebSocketCtor;
21780
23397
  onUnexpectedClose;
21781
23398
  onReconnectError;
23399
+ effectHostUrl;
21782
23400
  debugHttp = process.env.GRANULAR_DEBUG_HTTP === "1";
21783
23401
  /** Sandbox-level effect registry: sandboxId → (effectKey@selector → ToolWithHandler) */
21784
23402
  sandboxEffects = /* @__PURE__ */ new Map();
@@ -21807,6 +23425,7 @@ var Granular = class _Granular {
21807
23425
  this.WebSocketCtor = options.WebSocketCtor;
21808
23426
  this.onUnexpectedClose = options.onUnexpectedClose;
21809
23427
  this.onReconnectError = options.onReconnectError;
23428
+ this.effectHostUrl = options.effectHostUrl;
21810
23429
  this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
21811
23430
  }
21812
23431
  /**
@@ -21818,13 +23437,27 @@ var Granular = class _Granular {
21818
23437
  registerEnvironmentImporter(ontologyNameOrId, handler) {
21819
23438
  this.ontologyImporters.set(ontologyNameOrId, handler);
21820
23439
  if (ontologyNameOrId.startsWith("sbx_")) {
21821
- this.sandboxImporters.set(ontologyNameOrId, handler);
23440
+ this.sandboxImporters.set(ontologyNameOrId, {
23441
+ handler,
23442
+ sourceOntology: ontologyNameOrId
23443
+ });
23444
+ return;
23445
+ }
23446
+ for (const [sandboxId, importer] of this.sandboxImporters.entries()) {
23447
+ if (importer.sourceOntology === ontologyNameOrId) {
23448
+ this.sandboxImporters.set(sandboxId, {
23449
+ handler,
23450
+ sourceOntology: ontologyNameOrId
23451
+ });
23452
+ }
21822
23453
  }
21823
23454
  }
21824
23455
  clearEnvironmentImporter(ontologyNameOrId) {
21825
23456
  this.ontologyImporters.delete(ontologyNameOrId);
21826
- if (ontologyNameOrId.startsWith("sbx_")) {
21827
- this.sandboxImporters.delete(ontologyNameOrId);
23457
+ for (const [sandboxId, importer] of this.sandboxImporters.entries()) {
23458
+ if (importer.sourceOntology === ontologyNameOrId) {
23459
+ this.sandboxImporters.delete(sandboxId);
23460
+ }
21828
23461
  }
21829
23462
  }
21830
23463
  /**
@@ -21963,6 +23596,30 @@ var Granular = class _Granular {
21963
23596
  permissions: options.permissions || options.user?.permissions || []
21964
23597
  });
21965
23598
  }
23599
+ /**
23600
+ * Run a registered environment importer against an environment that was
23601
+ * opened outside this SDK instance, for example by a delegated browser flow.
23602
+ *
23603
+ * This uses the same setup-run and queued record-import plumbing as
23604
+ * `openEnvironment()`: importer stages, expected object counts, and queued
23605
+ * import counters remain visible through `environment.setup` and
23606
+ * `getRecordImportSummary()`.
23607
+ */
23608
+ async runEnvironmentImporterForEnvironment(environmentId, options = {}) {
23609
+ const environmentData = await this.environments.get(environmentId);
23610
+ const environment = this.bindEnvironmentHandle(environmentData);
23611
+ const requestedOntology = options.ontology || environmentData.ontologyId || environmentData.sandboxId;
23612
+ return this.runEnvironmentImporter(
23613
+ {
23614
+ environment: environmentData,
23615
+ requestedOntology,
23616
+ sandboxId: environmentData.sandboxId,
23617
+ subjectId: environmentData.subjectId,
23618
+ setupTriggerReason: options.reason || "new_environment"
23619
+ },
23620
+ environment
23621
+ );
23622
+ }
21966
23623
  resolveRequestedTag(options, methodName) {
21967
23624
  const tag2 = options.tag?.trim() || options.tagName?.trim() || options.environment?.trim();
21968
23625
  if (!tag2) {
@@ -22185,22 +23842,46 @@ var Granular = class _Granular {
22185
23842
  return this.connectSession({ sessionId, clientId: options?.clientId });
22186
23843
  }
22187
23844
  resolveEnvironmentImporter(requestedOntology, sandboxId) {
22188
- const resolved = this.sandboxImporters.get(sandboxId) || this.ontologyImporters.get(requestedOntology);
22189
- if (resolved && !this.sandboxImporters.has(sandboxId) && this.ontologyImporters.get(requestedOntology) === resolved) {
22190
- this.sandboxImporters.set(sandboxId, resolved);
23845
+ const sandboxImporter = this.sandboxImporters.get(sandboxId);
23846
+ if (sandboxImporter) {
23847
+ const sourceImporter = this.ontologyImporters.get(
23848
+ sandboxImporter.sourceOntology
23849
+ );
23850
+ if (sourceImporter === sandboxImporter.handler) {
23851
+ return sandboxImporter.handler;
23852
+ }
23853
+ this.sandboxImporters.delete(sandboxId);
22191
23854
  }
22192
- return resolved;
23855
+ const ontologyImporter = this.ontologyImporters.get(requestedOntology);
23856
+ if (!ontologyImporter) {
23857
+ return void 0;
23858
+ }
23859
+ this.sandboxImporters.set(sandboxId, {
23860
+ handler: ontologyImporter,
23861
+ sourceOntology: requestedOntology
23862
+ });
23863
+ return ontologyImporter;
22193
23864
  }
22194
23865
  async maybeRunEnvironmentImporter(resolved, environment) {
22195
- if (!resolved.setupTriggerReason) {
22196
- return;
23866
+ const setupTriggerReason = resolved.setupTriggerReason;
23867
+ if (!setupTriggerReason) {
23868
+ return null;
22197
23869
  }
23870
+ return this.runEnvironmentImporter(
23871
+ {
23872
+ ...resolved,
23873
+ setupTriggerReason
23874
+ },
23875
+ environment
23876
+ );
23877
+ }
23878
+ async runEnvironmentImporter(resolved, environment) {
22198
23879
  const importer = this.resolveEnvironmentImporter(
22199
23880
  resolved.requestedOntology,
22200
23881
  resolved.sandboxId
22201
23882
  );
22202
23883
  if (!importer) {
22203
- return;
23884
+ return null;
22204
23885
  }
22205
23886
  const setupRun = await this.request(
22206
23887
  `/control/environments/${environment.environmentId}/setup-runs`,
@@ -22240,16 +23921,24 @@ var Granular = class _Granular {
22240
23921
  },
22241
23922
  importRecords: async (records, options) => environment.enqueueRecordImport(records, {
22242
23923
  batchSize: options?.batchSize,
23924
+ writeMode: options?.writeMode,
22243
23925
  setupRunId
22244
23926
  })
22245
23927
  };
22246
23928
  try {
22247
23929
  await importer(importerContext);
22248
- await updateSetupRun({ markHookCompleted: true });
23930
+ const completedSetupRun = await this.request(
23931
+ `/control/environment-setup-runs/${setupRunId}`,
23932
+ {
23933
+ method: "PATCH",
23934
+ body: JSON.stringify({ markHookCompleted: true })
23935
+ }
23936
+ );
22249
23937
  const refreshedEnvironment = await this.environments.get(
22250
23938
  environment.environmentId
22251
23939
  );
22252
23940
  environment.syncEnvironmentData(refreshedEnvironment);
23941
+ return completedSetupRun;
22253
23942
  } catch (error2) {
22254
23943
  await updateSetupRun({
22255
23944
  status: "failed",
@@ -22297,27 +23986,45 @@ var Granular = class _Granular {
22297
23986
  return effects2;
22298
23987
  }
22299
23988
  serializeEffect(effect) {
22300
- return {
23989
+ const serialized = {
22301
23990
  effectKey: computeEffectKey2(effect),
22302
23991
  name: effect.name,
22303
23992
  description: effect.description,
22304
23993
  inputSchema: effect.inputSchema,
22305
- outputSchema: effect.outputSchema,
22306
23994
  stability: effect.stability || "stable",
22307
- provenance: effect.provenance || { source: "custom" },
22308
- tags: effect.tags,
22309
- className: effect.className,
22310
- static: effect.static,
22311
- versionSelector: effect.versionSelector
23995
+ provenance: effect.provenance || { source: "custom" }
22312
23996
  };
23997
+ if (effect.outputSchema !== void 0) {
23998
+ serialized.outputSchema = effect.outputSchema;
23999
+ }
24000
+ if (effect.tags !== void 0) {
24001
+ serialized.tags = effect.tags;
24002
+ }
24003
+ if (effect.className !== void 0) {
24004
+ serialized.className = effect.className;
24005
+ }
24006
+ if (effect.static !== void 0) {
24007
+ serialized.static = effect.static;
24008
+ }
24009
+ if (effect.versionSelector !== void 0) {
24010
+ serialized.versionSelector = effect.versionSelector;
24011
+ }
24012
+ if (effect.metamodels !== void 0) {
24013
+ serialized.metamodels = effect.metamodels;
24014
+ }
24015
+ return serialized;
22313
24016
  }
22314
24017
  async publishSandboxEffectCatalog(host) {
22315
24018
  const effects2 = Array.from(
22316
24019
  this.getSandboxEffectMap(host.sandboxId).values()
22317
24020
  ).map((effect) => this.serializeEffect(effect));
22318
- const result = await host.wsClient.call("effects.publishCatalog", {
22319
- effects: effects2
22320
- });
24021
+ const result = await withTimeout(
24022
+ host.wsClient.call("effects.publishCatalog", {
24023
+ effects: effects2
24024
+ }),
24025
+ EFFECT_CATALOG_SYNC_TIMEOUT_MS,
24026
+ `effects.publishCatalog for sandbox ${host.sandboxId}`
24027
+ );
22321
24028
  const acceptedCount = typeof result?.acceptedCount === "number" ? result.acceptedCount : 0;
22322
24029
  const rejected = Array.isArray(result?.rejected) ? result.rejected : [];
22323
24030
  if (acceptedCount === 0 && rejected.length > 0) {
@@ -22336,8 +24043,26 @@ var Granular = class _Granular {
22336
24043
  }
22337
24044
  }
22338
24045
  async syncSandboxEffectCatalog(sandboxId) {
22339
- const host = await this.ensureSandboxEffectHost(sandboxId);
22340
- await this.publishSandboxEffectCatalog(host);
24046
+ let lastError;
24047
+ for (let attempt = 1; attempt <= EFFECT_CATALOG_SYNC_RETRY_COUNT; attempt += 1) {
24048
+ try {
24049
+ const host = await this.ensureSandboxEffectHost(sandboxId);
24050
+ await this.publishSandboxEffectCatalog(host);
24051
+ return;
24052
+ } catch (error2) {
24053
+ lastError = error2;
24054
+ this.disconnectSandboxEffectHost(sandboxId);
24055
+ if (attempt === EFFECT_CATALOG_SYNC_RETRY_COUNT || !isRetryableEffectRegistrationError(error2)) {
24056
+ throw error2;
24057
+ }
24058
+ console.warn(
24059
+ `[Granular] Retrying effect registration for sandbox ${sandboxId} after transient failure (${attempt}/${EFFECT_CATALOG_SYNC_RETRY_COUNT - 1} retries used):`,
24060
+ error2
24061
+ );
24062
+ await sleep(EFFECT_CATALOG_SYNC_RETRY_DELAY_MS * attempt);
24063
+ }
24064
+ }
24065
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
22341
24066
  }
22342
24067
  recoverEffectHost(host, error2) {
22343
24068
  if (host.recovering) {
@@ -22430,7 +24155,8 @@ var Granular = class _Granular {
22430
24155
  this.apiUrl,
22431
24156
  sandboxId,
22432
24157
  effectClientId,
22433
- clientId
24158
+ clientId,
24159
+ this.effectHostUrl
22434
24160
  ),
22435
24161
  sessionId: `effect-host:${effectClientId}`,
22436
24162
  token: this.apiKey,
@@ -22466,7 +24192,11 @@ var Granular = class _Granular {
22466
24192
  wsClient.on("disconnect", () => {
22467
24193
  this.stopEffectHostHeartbeat(host);
22468
24194
  });
22469
- await wsClient.connect();
24195
+ await withTimeout(
24196
+ wsClient.connect(),
24197
+ EFFECT_HOST_CONNECT_TIMEOUT_MS,
24198
+ `effect host WebSocket connect for sandbox ${sandboxId}`
24199
+ );
22470
24200
  await this.synchronizeEffectHost(host);
22471
24201
  this.sandboxEffectHosts.set(sandboxId, host);
22472
24202
  return host;
@@ -22589,7 +24319,7 @@ var Granular = class _Granular {
22589
24319
  /**
22590
24320
  * Ensure a permission profile exists for a sandbox, creating it if needed.
22591
24321
  * If profileName matches an existing profile name, returns its ID.
22592
- * Otherwise, creates a new profile with default allow-all rules.
24322
+ * Otherwise, creates a v1 source-profile file shape with an allow default.
22593
24323
  */
22594
24324
  async ensurePermissionProfile(sandboxId, profileName) {
22595
24325
  try {
@@ -22603,8 +24333,11 @@ var Granular = class _Granular {
22603
24333
  const created = await this.permissionProfiles.create(sandboxId, {
22604
24334
  name: profileName,
22605
24335
  rules: {
22606
- effects: { allow: ["*"] },
22607
- resources: { allow: ["*"] }
24336
+ schemaVersion: 1,
24337
+ name: profileName,
24338
+ description: profileName === "allow-all" ? "Every declared action is visible unless a manifest policy denies it." : `Generated permission profile ${profileName}`,
24339
+ defaults: { actionPolicy: "allow" },
24340
+ actions: []
22608
24341
  }
22609
24342
  });
22610
24343
  return created.permissionProfileId;
@@ -22677,33 +24410,63 @@ var Granular = class _Granular {
22677
24410
  * Permission Profile management for sandboxes
22678
24411
  */
22679
24412
  get permissionProfiles() {
24413
+ const profileSourceFromRecord = (record) => {
24414
+ const profile = record.profile || record.rules || {};
24415
+ return {
24416
+ ...profile,
24417
+ schemaVersion: profile.schemaVersion || 1,
24418
+ name: profile.name || record.name,
24419
+ description: profile.description || record.description
24420
+ };
24421
+ };
22680
24422
  return {
22681
24423
  list: async (sandboxId) => {
22682
24424
  const result = await this.request(
22683
- `/control/sandboxes/${sandboxId}/permission-profiles`
24425
+ `/control/sandboxes/${sandboxId}/permission-profile-sources`
22684
24426
  );
22685
24427
  return result.items;
22686
24428
  },
22687
24429
  get: async (sandboxId, profileId) => {
22688
- return this.request(
22689
- `/control/sandboxes/${sandboxId}/permission-profiles/${profileId}`
24430
+ const result = await this.request(
24431
+ `/control/sandboxes/${sandboxId}/permission-profile-sources`
24432
+ );
24433
+ const profile = result.items.find(
24434
+ (item) => item.permissionProfileId === profileId || item.name === profileId
22690
24435
  );
24436
+ if (!profile) {
24437
+ throw new Error(`Permission profile source not found: ${profileId}`);
24438
+ }
24439
+ return profile;
22691
24440
  },
22692
24441
  create: async (sandboxId, data) => {
22693
- return this.request(
22694
- `/control/sandboxes/${sandboxId}/permission-profiles`,
24442
+ const profile = {
24443
+ ...data.rules,
24444
+ schemaVersion: 1,
24445
+ name: data.name
24446
+ };
24447
+ const existingProfiles = await this.permissionProfiles.list(sandboxId);
24448
+ const profiles = [
24449
+ ...existingProfiles.filter((existing) => existing.name !== data.name).map((existing) => profileSourceFromRecord(existing)),
24450
+ profile
24451
+ ];
24452
+ const result = await this.request(
24453
+ `/control/sandboxes/${sandboxId}/permission-profile-sources`,
22695
24454
  {
22696
- method: "POST",
22697
- body: JSON.stringify(data)
24455
+ method: "PUT",
24456
+ body: JSON.stringify({ profiles })
22698
24457
  }
22699
24458
  );
24459
+ const synced = result.items.find((item) => item.name === data.name) || result.items[0];
24460
+ if (!synced) {
24461
+ throw new Error(
24462
+ `Permission profile source sync did not return ${data.name}`
24463
+ );
24464
+ }
24465
+ return synced;
22700
24466
  },
22701
- delete: async (sandboxId, profileId) => {
22702
- return this.request(
22703
- `/control/sandboxes/${sandboxId}/permission-profiles/${profileId}`,
22704
- {
22705
- method: "DELETE"
22706
- }
24467
+ delete: async (_sandboxId, _profileId) => {
24468
+ throw new Error(
24469
+ "Permission profile sources are updated by syncing the desired source set."
22707
24470
  );
22708
24471
  }
22709
24472
  };
@@ -22916,8 +24679,8 @@ var Granular = class _Granular {
22916
24679
  /**
22917
24680
  * Make an authenticated API request
22918
24681
  */
22919
- async request(path6, options = {}) {
22920
- const url = `${this.httpUrl}${path6}`;
24682
+ async request(path7, options = {}) {
24683
+ const url = `${this.httpUrl}${path7}`;
22921
24684
  if (this.debugHttp) {
22922
24685
  console.log(`[SDK] Requesting: ${url}`);
22923
24686
  }
@@ -22961,7 +24724,7 @@ var Granular = class _Granular {
22961
24724
  // src/cli/commands/runtime-shared.ts
22962
24725
  function normalizePermissions(raw) {
22963
24726
  const values = raw?.split(",").map((value) => value.trim()).filter(Boolean);
22964
- return values && values.length > 0 ? values : ["default"];
24727
+ return values && values.length > 0 ? values : ["allow-all"];
22965
24728
  }
22966
24729
  function createGranularClient() {
22967
24730
  const config = resolveConfig({ requireApiKey: true });
@@ -23021,7 +24784,7 @@ async function resolveEnvironmentData(granular, options) {
23021
24784
  ontology: ontologyId,
23022
24785
  environment: environmentName,
23023
24786
  userId: options.userId ?? "granular-cli",
23024
- permissions: options.permissions ?? ["default"]
24787
+ permissions: options.permissions ?? ["allow-all"]
23025
24788
  });
23026
24789
  return await granular.environments.get(connection.environmentId);
23027
24790
  }
@@ -23061,7 +24824,7 @@ async function connectRuntime(options) {
23061
24824
  ontology: ontologyId,
23062
24825
  environment: options.environment ?? "dev",
23063
24826
  userId: options.userId ?? "granular-cli",
23064
- permissions: options.permissions ?? ["default"]
24827
+ permissions: options.permissions ?? ["allow-all"]
23065
24828
  });
23066
24829
  const environment = await environmentHandle.createSession();
23067
24830
  return {
@@ -23222,13 +24985,13 @@ async function sessionCreateCommand(options) {
23222
24985
  printHeader();
23223
24986
  }
23224
24987
  const { granular } = createGranularClient();
23225
- const permissions = normalizePermissions(options.permissions);
24988
+ const permissions2 = normalizePermissions(options.permissions);
23226
24989
  const envData = await resolveEnvironmentData(granular, {
23227
24990
  ontology: options.ontology,
23228
24991
  environment: requestedEnvironment,
23229
24992
  environmentId: options.environmentId,
23230
24993
  userId: options.user,
23231
- permissions,
24994
+ permissions: permissions2,
23232
24995
  createIfMissing: true
23233
24996
  });
23234
24997
  const environment = await granular.createSession({
@@ -23553,6 +25316,101 @@ async function jobRunCommand(options) {
23553
25316
  );
23554
25317
  }
23555
25318
 
25319
+ // src/cli/commands/permissions.ts
25320
+ function parseJsonObject(value) {
25321
+ if (!value) return {};
25322
+ const parsed = JSON.parse(value);
25323
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
25324
+ throw new Error("Expected a JSON object");
25325
+ }
25326
+ return parsed;
25327
+ }
25328
+ function resolveEffectKey(manifest, action, on) {
25329
+ const effects2 = extractManifestEffects(manifest);
25330
+ const matches = effects2.filter((effect) => {
25331
+ if (effect.name !== action) return false;
25332
+ return on ? effect.attachedClass === on : !effect.attachedClass;
25333
+ });
25334
+ if (matches.length === 1) return matches[0].effectKey;
25335
+ if (matches.length === 0) {
25336
+ throw new Error(
25337
+ on ? `Action "${action}" on "${on}" was not found.` : `Global action "${action}" was not found.`
25338
+ );
25339
+ }
25340
+ throw new Error(`Action "${action}" is ambiguous. Add --on.`);
25341
+ }
25342
+ async function permissionsValidateCommand() {
25343
+ const config = resolveConfig({ requireProject: true });
25344
+ const manifest = config.project.manifest;
25345
+ const profiles = readPermissionProfileFiles();
25346
+ const result = await compilePolicyBundle({
25347
+ sandboxId: config.sandboxId,
25348
+ effects: extractManifestEffects(manifest),
25349
+ profileSources: profiles
25350
+ });
25351
+ if (result.errors.length > 0) {
25352
+ error("Permission policy validation failed:");
25353
+ for (const error2 of result.errors) error(` ${error2}`);
25354
+ process.exit(1);
25355
+ }
25356
+ success(
25357
+ `Validated ${Object.keys(result.bundle.profiles).length} permission profile(s).`
25358
+ );
25359
+ if (result.warnings.length > 0) {
25360
+ for (const warning of result.warnings) warn(warning);
25361
+ }
25362
+ }
25363
+ async function permissionsPreviewCommand(options) {
25364
+ const config = resolveConfig({
25365
+ requireApiKey: Boolean(options.versionId),
25366
+ requireProject: true
25367
+ });
25368
+ if (!options.profile || !options.action) {
25369
+ throw new Error("permissions preview requires --profile and --action");
25370
+ }
25371
+ const requestInput = parseJsonObject(options.input);
25372
+ const objectContext = parseJsonObject(options.object);
25373
+ const stateMachines = parseJsonObject(options.stateMachines);
25374
+ const effectKey = resolveEffectKey(
25375
+ config.project.manifest,
25376
+ options.action,
25377
+ options.on
25378
+ );
25379
+ if (options.versionId) {
25380
+ const api = new ApiClient(config.apiKey, config.apiUrl);
25381
+ const decision2 = await api.evaluateVersionPolicy(options.versionId, {
25382
+ permissionProfileName: options.profile,
25383
+ effectKey,
25384
+ input: requestInput,
25385
+ object: objectContext,
25386
+ stateMachines
25387
+ });
25388
+ keyValue({
25389
+ Decision: decision2.outcome,
25390
+ Matched: (decision2.matches || []).map((match) => match.reason || match.summary || match.id).join("; ") || "none"
25391
+ });
25392
+ return;
25393
+ }
25394
+ const result = await compilePolicyBundle({
25395
+ sandboxId: config.sandboxId,
25396
+ effects: extractManifestEffects(config.project.manifest),
25397
+ profileSources: readPermissionProfileFiles()
25398
+ });
25399
+ if (result.errors.length > 0) {
25400
+ throw new Error(result.errors.join("; "));
25401
+ }
25402
+ const decision = evaluateCompiledPolicyBundle({
25403
+ bundle: result.bundle,
25404
+ profileName: options.profile,
25405
+ effectKey,
25406
+ context: { input: requestInput, object: objectContext, stateMachines }
25407
+ });
25408
+ keyValue({
25409
+ Decision: decision.outcome,
25410
+ Matched: decision.matches.map((match) => match.reason || match.summary || match.id).join("; ") || "none"
25411
+ });
25412
+ }
25413
+
23556
25414
  // src/cli/commands/version.ts
23557
25415
  function formatVersionLabel(versionId, versionNumber) {
23558
25416
  return versionNumber != null ? `v${versionNumber}` : versionId;
@@ -23794,67 +25652,130 @@ program2.hook("preAction", () => {
23794
25652
  process.env.GRANULAR_ENDPOINT_MODE = mode;
23795
25653
  }
23796
25654
  });
23797
- 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) => {
23798
- try {
23799
- await initCommand(projectName, {
23800
- skipBuild: opts.skipBuild,
23801
- template: opts.template,
23802
- agentDocs: opts.agentDocs,
23803
- noAgentDocs: opts.noAgentDocs,
23804
- cursorHooks: opts.cursorHooks || opts.cursorAll,
23805
- cursorSubagents: opts.cursorSubagents || opts.cursorAll,
23806
- controlPlaneSession: opts.controlPlaneSession,
23807
- projectMode: opts.projectMode,
23808
- authStrategy: opts.authStrategy,
23809
- ide: opts.ide,
23810
- dataStrategy: opts.dataStrategy,
23811
- productSummary: opts.productSummary,
23812
- rolesSummary: opts.rolesSummary
23813
- });
23814
- } catch (err) {
23815
- error(err.message);
23816
- process.exit(1);
25655
+ program2.command("init [project-name]").description("Initialize a new Granular project").option("--skip-build", "Skip the initial build step").option(
25656
+ "--template <template>",
25657
+ "Starter ontology template: library|support|delivery"
25658
+ ).option(
25659
+ "--agent-docs",
25660
+ "Add AGENTS.md and docs/granular-manifest.md (for AI coding agents)"
25661
+ ).option("--no-agent-docs", "Skip agent documentation files").option("--cursor-hooks", "Emit optional Cursor hook files under .cursor/").option(
25662
+ "--cursor-subagents",
25663
+ "Emit optional Cursor subagent files under .cursor/"
25664
+ ).option("--cursor-all", "Emit both Cursor hooks and Cursor subagents").option(
25665
+ "--control-plane-session <sessionId>",
25666
+ "Attach the init flow to a control-plane onboarding session"
25667
+ ).option("--project-mode <mode>", "Onboarding context: new|existing").option(
25668
+ "--auth-strategy <strategy>",
25669
+ "Onboarding context: just-me|granular-auth|existing-auth"
25670
+ ).option("--ide <ide>", "Onboarding context: cursor|codex|claude-code").option("--data-strategy <strategy>", "Onboarding context: demo|real").option(
25671
+ "--product-summary <text>",
25672
+ "Onboarding context: product brief from the control plane"
25673
+ ).option(
25674
+ "--roles-summary <text>",
25675
+ "Onboarding context: roles and permission notes from the control plane"
25676
+ ).action(
25677
+ async (projectName, opts) => {
25678
+ try {
25679
+ await initCommand(projectName, {
25680
+ skipBuild: opts.skipBuild,
25681
+ template: opts.template,
25682
+ agentDocs: opts.agentDocs,
25683
+ noAgentDocs: opts.noAgentDocs,
25684
+ cursorHooks: opts.cursorHooks || opts.cursorAll,
25685
+ cursorSubagents: opts.cursorSubagents || opts.cursorAll,
25686
+ controlPlaneSession: opts.controlPlaneSession,
25687
+ projectMode: opts.projectMode,
25688
+ authStrategy: opts.authStrategy,
25689
+ ide: opts.ide,
25690
+ dataStrategy: opts.dataStrategy,
25691
+ productSummary: opts.productSummary,
25692
+ rolesSummary: opts.rolesSummary
25693
+ });
25694
+ } catch (err) {
25695
+ error(err.message);
25696
+ process.exit(1);
25697
+ }
23817
25698
  }
23818
- });
23819
- 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) => {
23820
- const n = Number.parseInt(value, 10);
23821
- return Number.isFinite(n) ? n : 180;
23822
- }).action(async (opts) => {
25699
+ );
25700
+ 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(
25701
+ "--timeout <seconds>",
25702
+ "Browser auth timeout in seconds",
25703
+ (value) => {
25704
+ const n = Number.parseInt(value, 10);
25705
+ return Number.isFinite(n) ? n : 180;
25706
+ }
25707
+ ).action(
25708
+ async (opts) => {
25709
+ try {
25710
+ await loginCommand({
25711
+ manual: opts.manual,
25712
+ apiKey: opts.apiKey,
25713
+ timeout: opts.timeout
25714
+ });
25715
+ } catch (err) {
25716
+ error(err.message);
25717
+ process.exit(1);
25718
+ }
25719
+ }
25720
+ );
25721
+ program2.command("pull <version-id>").description("Pull the manifest for a specific ontology version").action(async (versionId) => {
23823
25722
  try {
23824
- await loginCommand({
23825
- manual: opts.manual,
23826
- apiKey: opts.apiKey,
23827
- timeout: opts.timeout
23828
- });
25723
+ await pullCommand(versionId);
23829
25724
  } catch (err) {
23830
25725
  error(err.message);
23831
25726
  process.exit(1);
23832
25727
  }
23833
25728
  });
23834
- program2.command("pull <version-id>").description("Pull the manifest for a specific ontology version").action(async (versionId) => {
25729
+ program2.command("build").description(
25730
+ "Create or reuse the ontology version for your current manifest, then run its build"
25731
+ ).action(async () => {
23835
25732
  try {
23836
- await pullCommand(versionId);
25733
+ await buildCommand();
23837
25734
  } catch (err) {
23838
25735
  error(err.message);
23839
25736
  process.exit(1);
23840
25737
  }
23841
25738
  });
23842
- program2.command("build").description("Create or reuse the ontology version for your current manifest, then run its build").action(async () => {
25739
+ program2.command("deploy").description(
25740
+ "Push the current revision to dev; use --prod to move prod there too"
25741
+ ).option("--prod", "Also point prod to the current version").action(async (opts) => {
23843
25742
  try {
23844
- await buildCommand();
25743
+ await deployCommand({ prod: opts.prod });
23845
25744
  } catch (err) {
23846
25745
  error(err.message);
23847
25746
  process.exit(1);
23848
25747
  }
23849
25748
  });
23850
- 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) => {
25749
+ var permissions = program2.command("permissions").description("Validate and preview permission profile files");
25750
+ permissions.command("validate").description("Validate permissions/*.json against granular.json").action(async () => {
23851
25751
  try {
23852
- await deployCommand({ prod: opts.prod });
25752
+ await permissionsValidateCommand();
23853
25753
  } catch (err) {
23854
25754
  error(err.message);
23855
25755
  process.exit(1);
23856
25756
  }
23857
25757
  });
25758
+ 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(
25759
+ "--object <json>",
25760
+ "Target object fields JSON object for object-based conditions",
25761
+ "{}"
25762
+ ).option(
25763
+ "--state-machines <json>",
25764
+ 'Current state-machine states JSON object, e.g. {"lifecycle":"active"}',
25765
+ "{}"
25766
+ ).option(
25767
+ "--version-id <versionId>",
25768
+ "Evaluate against a compiled ontology version"
25769
+ ).action(
25770
+ async (opts) => {
25771
+ try {
25772
+ await permissionsPreviewCommand(opts);
25773
+ } catch (err) {
25774
+ error(err.message);
25775
+ process.exit(1);
25776
+ }
25777
+ }
25778
+ );
23858
25779
  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) => {
23859
25780
  try {
23860
25781
  await statusCommand(options);
@@ -23872,14 +25793,16 @@ version.command("current").description("Show the latest/current ontology version
23872
25793
  process.exit(1);
23873
25794
  }
23874
25795
  });
23875
- 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) => {
23876
- try {
23877
- await versionDiffCommand(versionId, againstVersionId, options);
23878
- } catch (err) {
23879
- error(err.message);
23880
- process.exit(1);
25796
+ 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(
25797
+ async (versionId, againstVersionId, options) => {
25798
+ try {
25799
+ await versionDiffCommand(versionId, againstVersionId, options);
25800
+ } catch (err) {
25801
+ error(err.message);
25802
+ process.exit(1);
25803
+ }
23881
25804
  }
23882
- });
25805
+ );
23883
25806
  var tag = program2.command("tag").description("Inspect and move release tags such as dev and prod");
23884
25807
  tag.command("list").description("List tags for the current ontology").action(async () => {
23885
25808
  try {
@@ -23938,7 +25861,9 @@ program2.command("dev").description("Start development mode (watch + auto-rebuil
23938
25861
  process.exit(1);
23939
25862
  }
23940
25863
  });
23941
- program2.command("document").description("Generate GRANULAR_SANDBOX.md (agent reference) from granular.json").action(async () => {
25864
+ program2.command("document").description(
25865
+ "Generate GRANULAR_SANDBOX.md (agent reference) from granular.json"
25866
+ ).action(async () => {
23942
25867
  try {
23943
25868
  await documentCommand();
23944
25869
  } catch (err) {
@@ -23946,83 +25871,232 @@ program2.command("document").description("Generate GRANULAR_SANDBOX.md (agent re
23946
25871
  process.exit(1);
23947
25872
  }
23948
25873
  });
23949
- 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) => {
23950
- try {
23951
- await simulateCommand(sandboxId, options);
23952
- } catch (err) {
23953
- error(err.message);
23954
- process.exit(1);
25874
+ program2.command("simulate [ontology-id]").description(
25875
+ "Open the Granular simulator in the browser for the current (or given) ontology"
25876
+ ).option(
25877
+ "--subject-id <subjectId>",
25878
+ "Open the simulator preloaded for a specific subject"
25879
+ ).option(
25880
+ "--environment <environment>",
25881
+ "Add an environment query parameter to the simulator URL"
25882
+ ).option(
25883
+ "--session <sessionId>",
25884
+ "Add a session query parameter to the simulator URL"
25885
+ ).option("--user <userId>", "Add a user query parameter to the simulator URL").option("--print-url", "Print the simulator URL without opening a browser").action(
25886
+ async (sandboxId, options) => {
25887
+ try {
25888
+ await simulateCommand(sandboxId, options);
25889
+ } catch (err) {
25890
+ error(err.message);
25891
+ process.exit(1);
25892
+ }
23955
25893
  }
23956
- });
25894
+ );
23957
25895
  var connect = program2.command("connect").description("Run connectivity checks against a live Granular ontology");
23958
- 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) => {
23959
- try {
23960
- await connectTestCommand(options);
23961
- } catch (err) {
23962
- error(err.message);
23963
- process.exit(1);
25896
+ connect.command("test").description("Create a real session and verify connect/disconnect works").option(
25897
+ "--ontology <ontologyId>",
25898
+ "Override the ontology id from .granularrc"
25899
+ ).option(
25900
+ "--environment <environment>",
25901
+ "Environment slot to connect to",
25902
+ "dev"
25903
+ ).option("--user <userId>", "External user id to connect as").option(
25904
+ "--permissions <list>",
25905
+ "Comma-separated permission profile to ensure",
25906
+ "allow-all"
25907
+ ).option("--json", "Print machine-readable JSON").action(
25908
+ async (options) => {
25909
+ try {
25910
+ await connectTestCommand(options);
25911
+ } catch (err) {
25912
+ error(err.message);
25913
+ process.exit(1);
25914
+ }
23964
25915
  }
23965
- });
23966
- var session = program2.command("session").description("Inspect live Granular sessions and their Automerge-backed state");
25916
+ );
25917
+ var session = program2.command("session").description(
25918
+ "Inspect live Granular sessions and their Automerge-backed state"
25919
+ );
23967
25920
  for (const [name, description, handler] of [
23968
25921
  ["heap", "Print the normalized session heap", sessionHeapCommand],
23969
25922
  ["doc", "Print the full session document", sessionDocCommand]
23970
25923
  ]) {
23971
- 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) => {
25924
+ session.command(name).description(description).option(
25925
+ "--ontology <ontologyId>",
25926
+ "Override the ontology id from .granularrc"
25927
+ ).option(
25928
+ "--environment <environment>",
25929
+ "Environment slot to connect to when no session id is provided",
25930
+ "dev"
25931
+ ).option(
25932
+ "--user <userId>",
25933
+ "External user id to connect as when no session id is provided"
25934
+ ).option(
25935
+ "--permissions <list>",
25936
+ "Comma-separated permission profile to ensure",
25937
+ "allow-all"
25938
+ ).option(
25939
+ "--session <sessionId>",
25940
+ "Connect to an existing session instead of creating a fresh one"
25941
+ ).option("--json", "Print machine-readable JSON").action(
25942
+ async (options) => {
25943
+ try {
25944
+ await handler(options);
25945
+ } catch (err) {
25946
+ error(err.message);
25947
+ process.exit(1);
25948
+ }
25949
+ }
25950
+ );
25951
+ }
25952
+ session.command("create").description(
25953
+ "Create a new session for an existing environment or environment slot"
25954
+ ).option(
25955
+ "--ontology <ontologyId>",
25956
+ "Override the ontology id from .granularrc"
25957
+ ).option(
25958
+ "--environment <environment>",
25959
+ "Environment slot name to resolve when --environment-id is not provided",
25960
+ "dev"
25961
+ ).option(
25962
+ "--environment-id <environmentId>",
25963
+ "Create a session for this exact environment id"
25964
+ ).option(
25965
+ "--user <userId>",
25966
+ "External user id to connect as when a named environment needs to be created"
25967
+ ).option(
25968
+ "--permissions <list>",
25969
+ "Comma-separated permission profile to ensure when a named environment needs to be created",
25970
+ "allow-all"
25971
+ ).option("--json", "Print machine-readable JSON").action(
25972
+ async (options) => {
23972
25973
  try {
23973
- await handler(options);
25974
+ await sessionCreateCommand(options);
23974
25975
  } catch (err) {
23975
25976
  error(err.message);
23976
25977
  process.exit(1);
23977
25978
  }
23978
- });
23979
- }
23980
- 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) => {
23981
- try {
23982
- await sessionCreateCommand(options);
23983
- } catch (err) {
23984
- error(err.message);
23985
- process.exit(1);
23986
25979
  }
23987
- });
23988
- 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) => {
23989
- try {
23990
- await sessionListCommand(options);
23991
- } catch (err) {
23992
- error(err.message);
23993
- process.exit(1);
25980
+ );
25981
+ session.command("list").description("List indexed sessions for an environment").option(
25982
+ "--ontology <ontologyId>",
25983
+ "Override the ontology id from .granularrc"
25984
+ ).option(
25985
+ "--environment <environment>",
25986
+ "Environment slot name to resolve when --environment-id is not provided",
25987
+ "dev"
25988
+ ).option(
25989
+ "--environment-id <environmentId>",
25990
+ "List sessions for this exact environment id"
25991
+ ).option(
25992
+ "--status <status>",
25993
+ "Session status filter: active|closed|all",
25994
+ "active"
25995
+ ).option("--json", "Print machine-readable JSON").action(
25996
+ async (options) => {
25997
+ try {
25998
+ await sessionListCommand(options);
25999
+ } catch (err) {
26000
+ error(err.message);
26001
+ process.exit(1);
26002
+ }
23994
26003
  }
23995
- });
26004
+ );
23996
26005
  var job = program2.command("job").description("Run real Granular runtime jobs from the CLI");
23997
- 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) => {
23998
- try {
23999
- await jobRunCommand(options);
24000
- } catch (err) {
24001
- error(err.message);
24002
- process.exit(1);
26006
+ job.command("run").description(
26007
+ "Submit a job from a file or inline code and wait for the result"
26008
+ ).option(
26009
+ "--ontology <ontologyId>",
26010
+ "Override the ontology id from .granularrc"
26011
+ ).option(
26012
+ "--environment <environment>",
26013
+ "Environment slot to connect to when no session id is provided",
26014
+ "dev"
26015
+ ).option(
26016
+ "--user <userId>",
26017
+ "External user id to connect as when no session id is provided"
26018
+ ).option(
26019
+ "--permissions <list>",
26020
+ "Comma-separated permission profile to ensure",
26021
+ "allow-all"
26022
+ ).option(
26023
+ "--session <sessionId>",
26024
+ "Connect to an existing session instead of creating a fresh one"
26025
+ ).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(
26026
+ async (options) => {
26027
+ try {
26028
+ await jobRunCommand(options);
26029
+ } catch (err) {
26030
+ error(err.message);
26031
+ process.exit(1);
26032
+ }
24003
26033
  }
24004
- });
24005
- 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) => {
24006
- try {
24007
- await graphqlCommand(options);
24008
- } catch (err) {
24009
- error(err.message);
24010
- process.exit(1);
26034
+ );
26035
+ program2.command("graphql").description(
26036
+ "Execute a real GraphQL query against the current Granular environment"
26037
+ ).requiredOption("--query <query>", "GraphQL query or mutation string").option("--variables <json>", "JSON object of GraphQL variables").option(
26038
+ "--ontology <ontologyId>",
26039
+ "Override the ontology id from .granularrc"
26040
+ ).option(
26041
+ "--environment <environment>",
26042
+ "Environment slot to connect to when no session id is provided",
26043
+ "dev"
26044
+ ).option(
26045
+ "--user <userId>",
26046
+ "External user id to connect as when no session id is provided"
26047
+ ).option(
26048
+ "--permissions <list>",
26049
+ "Comma-separated permission profile to ensure",
26050
+ "allow-all"
26051
+ ).option(
26052
+ "--session <sessionId>",
26053
+ "Connect to an existing session instead of creating a fresh one"
26054
+ ).option("--json", "Print machine-readable JSON").action(
26055
+ async (options) => {
26056
+ try {
26057
+ await graphqlCommand(options);
26058
+ } catch (err) {
26059
+ error(err.message);
26060
+ process.exit(1);
26061
+ }
24011
26062
  }
24012
- });
26063
+ );
24013
26064
  var effects = program2.command("effects").description("Inspect declared and live Granular effects for an environment");
24014
26065
  for (const [name, description, handler] of [
24015
26066
  ["list", "List the current effect catalog", effectsListCommand],
24016
- ["diff", "Compare declared effects to live ready handlers", effectsDiffCommand]
26067
+ [
26068
+ "diff",
26069
+ "Compare declared effects to live ready handlers",
26070
+ effectsDiffCommand
26071
+ ]
24017
26072
  ]) {
24018
- 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) => {
24019
- try {
24020
- await handler(options);
24021
- } catch (err) {
24022
- error(err.message);
24023
- process.exit(1);
26073
+ effects.command(name).description(description).option(
26074
+ "--ontology <ontologyId>",
26075
+ "Override the ontology id from .granularrc"
26076
+ ).option(
26077
+ "--environment <environment>",
26078
+ "Environment slot to connect to when no session id is provided",
26079
+ "dev"
26080
+ ).option(
26081
+ "--user <userId>",
26082
+ "External user id to connect as when no session id is provided"
26083
+ ).option(
26084
+ "--permissions <list>",
26085
+ "Comma-separated permission profile to ensure",
26086
+ "allow-all"
26087
+ ).option(
26088
+ "--session <sessionId>",
26089
+ "Connect to an existing session instead of creating a fresh one"
26090
+ ).option("--json", "Print machine-readable JSON").action(
26091
+ async (options) => {
26092
+ try {
26093
+ await handler(options);
26094
+ } catch (err) {
26095
+ error(err.message);
26096
+ process.exit(1);
26097
+ }
24024
26098
  }
24025
- });
26099
+ );
24026
26100
  }
24027
26101
  void program2.parseAsync(process.argv).then(() => {
24028
26102
  process.exit(0);