@hasna/loops 0.3.50 → 0.3.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -306,9 +306,11 @@ loops templates create-workflow custom-report \
306
306
  Use `--source builtin`, `--source custom`, or `--source all` on
307
307
  `list`, `show`, `render`, and `create-workflow` when automation needs an
308
308
  explicit source. Custom template ids and names cannot override built-ins.
309
- Custom templates fail closed for `danger-full-access`; use built-in templates
310
- with explicit break-glass handling for emergency workflows that need that
311
- sandbox.
309
+ Custom templates fail closed for `danger-full-access`, dangerous passthrough
310
+ arguments, and implicit Codewith/Codex full-access defaults. If a custom
311
+ Codewith/Codex template uses `permissionMode: "bypass"`, it must also set
312
+ `sandbox` to `workspace-write` or `read-only`. Use built-in templates with
313
+ explicit break-glass handling for emergency workflows that need full access.
312
314
 
313
315
  For event-driven task automation, `loops events handle todos-task` reads a
314
316
  Hasna event envelope from stdin or `HASNA_EVENT_JSON`, records a
package/dist/cli/index.js CHANGED
@@ -5908,7 +5908,7 @@ function buildScriptInventoryReport(store, opts = {}) {
5908
5908
  // package.json
5909
5909
  var package_default = {
5910
5910
  name: "@hasna/loops",
5911
- version: "0.3.50",
5911
+ version: "0.3.51",
5912
5912
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5913
5913
  type: "module",
5914
5914
  main: "dist/index.js",
@@ -6204,6 +6204,11 @@ var CUSTOM_TEMPLATE_VARIABLE_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
6204
6204
  var CUSTOM_TEMPLATE_VARIABLE_TYPES = new Set(["string", "number", "boolean", "json", "string[]"]);
6205
6205
  var CUSTOM_TEMPLATE_PLACEHOLDER = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
6206
6206
  var CUSTOM_TEMPLATE_EXACT_PLACEHOLDER = /^\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}$/;
6207
+ var CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS = [
6208
+ "danger-full-access",
6209
+ "dangerously-bypass",
6210
+ "dangerously-skip"
6211
+ ];
6207
6212
  function compactJson(value) {
6208
6213
  return JSON.stringify(value);
6209
6214
  }
@@ -6526,6 +6531,13 @@ function validateCustomTemplateId(id, label) {
6526
6531
  throw new Error(`${label} must match ${CUSTOM_TEMPLATE_ID_PATTERN.source}`);
6527
6532
  }
6528
6533
  }
6534
+ function optionalTemplateBoolean(value, label) {
6535
+ if (value === undefined)
6536
+ return;
6537
+ if (typeof value !== "boolean")
6538
+ throw new Error(`${label} must be a boolean`);
6539
+ return value;
6540
+ }
6529
6541
  function validateCustomTemplateVariables(value, label) {
6530
6542
  if (value === undefined)
6531
6543
  return [];
@@ -6548,32 +6560,57 @@ function validateCustomTemplateVariables(value, label) {
6548
6560
  if (type && !CUSTOM_TEMPLATE_VARIABLE_TYPES.has(type)) {
6549
6561
  throw new Error(`${entryLabel}.type must be one of ${[...CUSTOM_TEMPLATE_VARIABLE_TYPES].join(", ")}`);
6550
6562
  }
6551
- if (defaultValue === "danger-full-access") {
6552
- throw new Error(`${entryLabel}.default cannot be danger-full-access in a custom template`);
6563
+ if (defaultValue && CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS.some((pattern) => defaultValue.includes(pattern))) {
6564
+ throw new Error(`${entryLabel}.default cannot contain dangerous sandbox or bypass flags in a custom template`);
6553
6565
  }
6554
6566
  return {
6555
6567
  name,
6556
6568
  description,
6557
- required: entry.required === undefined ? undefined : Boolean(entry.required),
6569
+ required: optionalTemplateBoolean(entry.required, `${entryLabel}.required`),
6558
6570
  default: defaultValue,
6559
6571
  type
6560
6572
  };
6561
6573
  });
6562
6574
  }
6563
- function assertNoDangerFullAccess(value, label) {
6575
+ function hasDangerousArg(value) {
6576
+ return CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS.some((pattern) => value.includes(pattern));
6577
+ }
6578
+ function assertNoDangerousCustomTemplateScalars(value, label) {
6579
+ if (typeof value === "string") {
6580
+ if (hasDangerousArg(value)) {
6581
+ throw new Error(`${label} contains a dangerous sandbox or bypass flag; custom templates must not request danger-full-access`);
6582
+ }
6583
+ return;
6584
+ }
6564
6585
  if (!value || typeof value !== "object")
6565
6586
  return;
6566
6587
  if (Array.isArray(value)) {
6567
- value.forEach((entry, index) => assertNoDangerFullAccess(entry, `${label}[${index}]`));
6588
+ value.forEach((entry, index) => assertNoDangerousCustomTemplateScalars(entry, `${label}[${index}]`));
6568
6589
  return;
6569
6590
  }
6570
6591
  for (const [key, entry] of Object.entries(value)) {
6571
- if (key === "sandbox" && entry === "danger-full-access") {
6572
- throw new Error(`${label}.${key} uses danger-full-access; custom templates must not request danger-full-access`);
6573
- }
6574
- assertNoDangerFullAccess(entry, `${label}.${key}`);
6592
+ assertNoDangerousCustomTemplateScalars(entry, `${label}.${key}`);
6575
6593
  }
6576
6594
  }
6595
+ function assertNoImplicitDangerFullAccess(value, label) {
6596
+ if (!value || typeof value !== "object")
6597
+ return;
6598
+ if (Array.isArray(value)) {
6599
+ value.forEach((entry, index) => assertNoImplicitDangerFullAccess(entry, `${label}[${index}]`));
6600
+ return;
6601
+ }
6602
+ const object = value;
6603
+ if (object.type === "agent" && (object.provider === "codewith" || object.provider === "codex") && object.permissionMode === "bypass" && object.sandbox === undefined) {
6604
+ throw new Error(`${label} uses permissionMode=bypass for ${object.provider} without an explicit sandbox; set sandbox=workspace-write or read-only`);
6605
+ }
6606
+ for (const [key, entry] of Object.entries(object)) {
6607
+ assertNoImplicitDangerFullAccess(entry, `${label}.${key}`);
6608
+ }
6609
+ }
6610
+ function assertCustomTemplateSafety(value, label) {
6611
+ assertNoDangerousCustomTemplateScalars(value, label);
6612
+ assertNoImplicitDangerFullAccess(value, label);
6613
+ }
6577
6614
  function customTemplateDefinitionFromJson(value, sourcePath) {
6578
6615
  assertRecord(value, sourcePath);
6579
6616
  const id = assertTemplateString(value.id, `${sourcePath}.id`);
@@ -6585,7 +6622,7 @@ function customTemplateDefinitionFromJson(value, sourcePath) {
6585
6622
  if (value.workflow === undefined)
6586
6623
  throw new Error(`${sourcePath}.workflow is required`);
6587
6624
  assertRecord(value.workflow, `${sourcePath}.workflow`);
6588
- assertNoDangerFullAccess(value.workflow, `${sourcePath}.workflow`);
6625
+ assertCustomTemplateSafety(value.workflow, `${sourcePath}.workflow`);
6589
6626
  return { id, name, description, kind, variables, workflow: value.workflow };
6590
6627
  }
6591
6628
  function customTemplateSummary(definition, sourcePath) {
@@ -6626,11 +6663,11 @@ function assertNoTemplateCollisions(entries) {
6626
6663
  }
6627
6664
  }
6628
6665
  }
6629
- function loadCustomLoopTemplates() {
6666
+ function loadCustomLoopTemplatesRaw() {
6630
6667
  const dir = customLoopTemplatesDir();
6631
6668
  if (!existsSync4(dir))
6632
6669
  return [];
6633
- const entries = readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
6670
+ return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
6634
6671
  const file = join5(dir, entry.name);
6635
6672
  if (entry.isSymbolicLink())
6636
6673
  throw new Error(`refusing symlinked custom template file: ${file}`);
@@ -6638,6 +6675,9 @@ function loadCustomLoopTemplates() {
6638
6675
  throw new Error(`custom template registry entry is not a regular file: ${file}`);
6639
6676
  return readCustomTemplateFile(file);
6640
6677
  });
6678
+ }
6679
+ function loadCustomLoopTemplates() {
6680
+ const entries = loadCustomLoopTemplatesRaw();
6641
6681
  assertNoTemplateCollisions(entries);
6642
6682
  return entries;
6643
6683
  }
@@ -6732,21 +6772,26 @@ function renderCustomTemplateNode(value, values, templateId) {
6732
6772
  function renderCustomLoopTemplate(entry, values) {
6733
6773
  const renderedValues = customTemplateValues(entry.definition, values);
6734
6774
  const rendered = renderCustomTemplateNode(entry.definition.workflow, renderedValues, entry.definition.id);
6735
- assertNoDangerFullAccess(rendered, `custom template ${entry.definition.id}.workflow`);
6736
- return workflowBodyFromJson(rendered);
6775
+ assertCustomTemplateSafety(rendered, `custom template ${entry.definition.id}.workflow`);
6776
+ const workflow = workflowBodyFromJson(rendered);
6777
+ assertCustomTemplateSafety(workflow, `custom template ${entry.definition.id}.workflow`);
6778
+ return workflow;
6737
6779
  }
6738
6780
  function validateCustomLoopTemplateFile(file) {
6739
- const entry = readCustomTemplateFile(resolve(file));
6740
- assertNoTemplateCollisions([entry]);
6781
+ const source = resolve(file);
6782
+ const entry = readCustomTemplateFile(source);
6783
+ const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== source);
6784
+ assertNoTemplateCollisions([...existing, entry]);
6741
6785
  return structuredClone(entry.summary);
6742
6786
  }
6743
6787
  function importCustomLoopTemplate(file, opts = {}) {
6744
6788
  const source = resolve(file);
6745
6789
  const entry = readCustomTemplateFile(source);
6746
- assertNoTemplateCollisions([entry]);
6747
6790
  const dir = ensureCustomLoopTemplatesDir();
6748
6791
  const destination = join5(dir, `${entry.definition.id}.json`);
6749
6792
  const replaced = existsSync4(destination);
6793
+ const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== resolve(destination));
6794
+ assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
6750
6795
  if (replaced) {
6751
6796
  const stat = lstatSync(destination);
6752
6797
  if (!stat.isFile() || stat.isSymbolicLink())
@@ -5240,7 +5240,7 @@ function enableStartup(result) {
5240
5240
  // package.json
5241
5241
  var package_default = {
5242
5242
  name: "@hasna/loops",
5243
- version: "0.3.50",
5243
+ version: "0.3.51",
5244
5244
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5245
5245
  type: "module",
5246
5246
  main: "dist/index.js",
package/dist/index.js CHANGED
@@ -5183,6 +5183,11 @@ var CUSTOM_TEMPLATE_VARIABLE_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
5183
5183
  var CUSTOM_TEMPLATE_VARIABLE_TYPES = new Set(["string", "number", "boolean", "json", "string[]"]);
5184
5184
  var CUSTOM_TEMPLATE_PLACEHOLDER = /\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
5185
5185
  var CUSTOM_TEMPLATE_EXACT_PLACEHOLDER = /^\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}$/;
5186
+ var CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS = [
5187
+ "danger-full-access",
5188
+ "dangerously-bypass",
5189
+ "dangerously-skip"
5190
+ ];
5186
5191
  function compactJson(value) {
5187
5192
  return JSON.stringify(value);
5188
5193
  }
@@ -5505,6 +5510,13 @@ function validateCustomTemplateId(id, label) {
5505
5510
  throw new Error(`${label} must match ${CUSTOM_TEMPLATE_ID_PATTERN.source}`);
5506
5511
  }
5507
5512
  }
5513
+ function optionalTemplateBoolean(value, label) {
5514
+ if (value === undefined)
5515
+ return;
5516
+ if (typeof value !== "boolean")
5517
+ throw new Error(`${label} must be a boolean`);
5518
+ return value;
5519
+ }
5508
5520
  function validateCustomTemplateVariables(value, label) {
5509
5521
  if (value === undefined)
5510
5522
  return [];
@@ -5527,32 +5539,57 @@ function validateCustomTemplateVariables(value, label) {
5527
5539
  if (type && !CUSTOM_TEMPLATE_VARIABLE_TYPES.has(type)) {
5528
5540
  throw new Error(`${entryLabel}.type must be one of ${[...CUSTOM_TEMPLATE_VARIABLE_TYPES].join(", ")}`);
5529
5541
  }
5530
- if (defaultValue === "danger-full-access") {
5531
- throw new Error(`${entryLabel}.default cannot be danger-full-access in a custom template`);
5542
+ if (defaultValue && CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS.some((pattern) => defaultValue.includes(pattern))) {
5543
+ throw new Error(`${entryLabel}.default cannot contain dangerous sandbox or bypass flags in a custom template`);
5532
5544
  }
5533
5545
  return {
5534
5546
  name,
5535
5547
  description,
5536
- required: entry.required === undefined ? undefined : Boolean(entry.required),
5548
+ required: optionalTemplateBoolean(entry.required, `${entryLabel}.required`),
5537
5549
  default: defaultValue,
5538
5550
  type
5539
5551
  };
5540
5552
  });
5541
5553
  }
5542
- function assertNoDangerFullAccess(value, label) {
5554
+ function hasDangerousArg(value) {
5555
+ return CUSTOM_TEMPLATE_DANGEROUS_ARG_PATTERNS.some((pattern) => value.includes(pattern));
5556
+ }
5557
+ function assertNoDangerousCustomTemplateScalars(value, label) {
5558
+ if (typeof value === "string") {
5559
+ if (hasDangerousArg(value)) {
5560
+ throw new Error(`${label} contains a dangerous sandbox or bypass flag; custom templates must not request danger-full-access`);
5561
+ }
5562
+ return;
5563
+ }
5543
5564
  if (!value || typeof value !== "object")
5544
5565
  return;
5545
5566
  if (Array.isArray(value)) {
5546
- value.forEach((entry, index) => assertNoDangerFullAccess(entry, `${label}[${index}]`));
5567
+ value.forEach((entry, index) => assertNoDangerousCustomTemplateScalars(entry, `${label}[${index}]`));
5547
5568
  return;
5548
5569
  }
5549
5570
  for (const [key, entry] of Object.entries(value)) {
5550
- if (key === "sandbox" && entry === "danger-full-access") {
5551
- throw new Error(`${label}.${key} uses danger-full-access; custom templates must not request danger-full-access`);
5552
- }
5553
- assertNoDangerFullAccess(entry, `${label}.${key}`);
5571
+ assertNoDangerousCustomTemplateScalars(entry, `${label}.${key}`);
5554
5572
  }
5555
5573
  }
5574
+ function assertNoImplicitDangerFullAccess(value, label) {
5575
+ if (!value || typeof value !== "object")
5576
+ return;
5577
+ if (Array.isArray(value)) {
5578
+ value.forEach((entry, index) => assertNoImplicitDangerFullAccess(entry, `${label}[${index}]`));
5579
+ return;
5580
+ }
5581
+ const object = value;
5582
+ if (object.type === "agent" && (object.provider === "codewith" || object.provider === "codex") && object.permissionMode === "bypass" && object.sandbox === undefined) {
5583
+ throw new Error(`${label} uses permissionMode=bypass for ${object.provider} without an explicit sandbox; set sandbox=workspace-write or read-only`);
5584
+ }
5585
+ for (const [key, entry] of Object.entries(object)) {
5586
+ assertNoImplicitDangerFullAccess(entry, `${label}.${key}`);
5587
+ }
5588
+ }
5589
+ function assertCustomTemplateSafety(value, label) {
5590
+ assertNoDangerousCustomTemplateScalars(value, label);
5591
+ assertNoImplicitDangerFullAccess(value, label);
5592
+ }
5556
5593
  function customTemplateDefinitionFromJson(value, sourcePath) {
5557
5594
  assertRecord(value, sourcePath);
5558
5595
  const id = assertTemplateString(value.id, `${sourcePath}.id`);
@@ -5564,7 +5601,7 @@ function customTemplateDefinitionFromJson(value, sourcePath) {
5564
5601
  if (value.workflow === undefined)
5565
5602
  throw new Error(`${sourcePath}.workflow is required`);
5566
5603
  assertRecord(value.workflow, `${sourcePath}.workflow`);
5567
- assertNoDangerFullAccess(value.workflow, `${sourcePath}.workflow`);
5604
+ assertCustomTemplateSafety(value.workflow, `${sourcePath}.workflow`);
5568
5605
  return { id, name, description, kind, variables, workflow: value.workflow };
5569
5606
  }
5570
5607
  function customTemplateSummary(definition, sourcePath) {
@@ -5605,11 +5642,11 @@ function assertNoTemplateCollisions(entries) {
5605
5642
  }
5606
5643
  }
5607
5644
  }
5608
- function loadCustomLoopTemplates() {
5645
+ function loadCustomLoopTemplatesRaw() {
5609
5646
  const dir = customLoopTemplatesDir();
5610
5647
  if (!existsSync3(dir))
5611
5648
  return [];
5612
- const entries = readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
5649
+ return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.name.endsWith(".json")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
5613
5650
  const file = join5(dir, entry.name);
5614
5651
  if (entry.isSymbolicLink())
5615
5652
  throw new Error(`refusing symlinked custom template file: ${file}`);
@@ -5617,6 +5654,9 @@ function loadCustomLoopTemplates() {
5617
5654
  throw new Error(`custom template registry entry is not a regular file: ${file}`);
5618
5655
  return readCustomTemplateFile(file);
5619
5656
  });
5657
+ }
5658
+ function loadCustomLoopTemplates() {
5659
+ const entries = loadCustomLoopTemplatesRaw();
5620
5660
  assertNoTemplateCollisions(entries);
5621
5661
  return entries;
5622
5662
  }
@@ -5711,21 +5751,26 @@ function renderCustomTemplateNode(value, values, templateId) {
5711
5751
  function renderCustomLoopTemplate(entry, values) {
5712
5752
  const renderedValues = customTemplateValues(entry.definition, values);
5713
5753
  const rendered = renderCustomTemplateNode(entry.definition.workflow, renderedValues, entry.definition.id);
5714
- assertNoDangerFullAccess(rendered, `custom template ${entry.definition.id}.workflow`);
5715
- return workflowBodyFromJson(rendered);
5754
+ assertCustomTemplateSafety(rendered, `custom template ${entry.definition.id}.workflow`);
5755
+ const workflow = workflowBodyFromJson(rendered);
5756
+ assertCustomTemplateSafety(workflow, `custom template ${entry.definition.id}.workflow`);
5757
+ return workflow;
5716
5758
  }
5717
5759
  function validateCustomLoopTemplateFile(file) {
5718
- const entry = readCustomTemplateFile(resolve(file));
5719
- assertNoTemplateCollisions([entry]);
5760
+ const source = resolve(file);
5761
+ const entry = readCustomTemplateFile(source);
5762
+ const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== source);
5763
+ assertNoTemplateCollisions([...existing, entry]);
5720
5764
  return structuredClone(entry.summary);
5721
5765
  }
5722
5766
  function importCustomLoopTemplate(file, opts = {}) {
5723
5767
  const source = resolve(file);
5724
5768
  const entry = readCustomTemplateFile(source);
5725
- assertNoTemplateCollisions([entry]);
5726
5769
  const dir = ensureCustomLoopTemplatesDir();
5727
5770
  const destination = join5(dir, `${entry.definition.id}.json`);
5728
5771
  const replaced = existsSync3(destination);
5772
+ const existing = loadCustomLoopTemplatesRaw().filter((template) => resolve(template.path) !== resolve(destination));
5773
+ assertNoTemplateCollisions([...existing, { ...entry, path: destination, summary: customTemplateSummary(entry.definition, destination) }]);
5729
5774
  if (replaced) {
5730
5775
  const stat = lstatSync(destination);
5731
5776
  if (!stat.isFile() || stat.isSymbolicLink())
package/docs/USAGE.md CHANGED
@@ -309,9 +309,11 @@ loops templates create-workflow custom-report \
309
309
  Use `--source builtin`, `--source custom`, or `--source all` on
310
310
  `list`, `show`, `render`, and `create-workflow` when automation needs an
311
311
  explicit source. Custom template ids and names cannot override built-ins.
312
- Custom templates fail closed for `danger-full-access`; use built-in templates
313
- with explicit break-glass handling for emergency workflows that need that
314
- sandbox.
312
+ Custom templates fail closed for `danger-full-access`, dangerous passthrough
313
+ arguments, and implicit Codewith/Codex full-access defaults. If a custom
314
+ Codewith/Codex template uses `permissionMode: "bypass"`, it must also set
315
+ `sandbox` to `workspace-write` or `read-only`. Use built-in templates with
316
+ explicit break-glass handling for emergency workflows that need full access.
315
317
 
316
318
  Repo-mutating task/event routes should set `worktreeMode=required` so the
317
319
  workflow fails fast instead of falling back to the main checkout. When
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/loops",
3
- "version": "0.3.50",
3
+ "version": "0.3.51",
4
4
  "description": "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",