gowalk-cicd 1.0.151 → 1.0.153

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.
@@ -1 +1 @@
1
- 1.0.151
1
+ 1.0.153
@@ -1 +1 @@
1
- 1.0.151
1
+ 1.0.153
@@ -1 +1 @@
1
- 1.0.151
1
+ 1.0.153
@@ -10,7 +10,8 @@ FAILURES = frozenset({
10
10
  'pool_provenance_unknown', 'pool_inventory_invalid', 'host_routes_unsupported',
11
11
  'host_rules_unsupported', 'host_addresses_invalid', 'resolver_inventory_invalid',
12
12
  'resolver_upstream_unknown',
13
- })
13
+ }) | frozenset('host_rules_unsupported_' + part for part in
14
+ ('count', 'fields', 'priority', 'source', 'table', 'structure', 'fwmark', 'fwmask', 'action', 'duplicate'))
14
15
  PRIVATE = tuple(ipaddress.ip_network(value) for value in ('10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'))
15
16
  DEVICE = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.:-]{0,14}')
16
17
 
@@ -195,7 +196,7 @@ def validate_rules(rows):
195
196
  """Admit ordinary Linux rules or the exact Tailscale v1.88.2 baseIPRules chain."""
196
197
  # https://github.com/tailscale/tailscale/blob/v1.88.2/wgengine/router/router_linux.go
197
198
  if not isinstance(rows, list) or len(rows) not in (3, 7):
198
- raise InventoryFailure('host_rules_unsupported')
199
+ raise InventoryFailure('host_rules_unsupported_count')
199
200
  expected = [{'priority': p, 'src': 'all', 'table': t}
200
201
  for p, t in ((0, 'local'), (32766, 'main'), (32767, 'default'))]
201
202
  if len(rows) == 7:
@@ -203,23 +204,36 @@ def validate_rules(rows):
203
204
  expected += [dict(marked, priority=5210, table='main'), dict(marked, priority=5230, table='default'),
204
205
  dict(marked, priority=5250, action='unreachable'), {'priority': 5270, 'src': 'all', 'table': 52}]
205
206
  actual = []
206
- tables = {255: 'local', 254: 'main', 253: 'default'}
207
+ # iproute2 iprule.c prints table names/numbers as JSON strings, including Tailscale's "52".
208
+ tables = {255: 'local', 254: 'main', 253: 'default',
209
+ '255': 'local', '254': 'main', '253': 'default', '52': 52}
207
210
  for row in rows:
208
- if (not isinstance(row, dict) or set(row) - {'priority', 'src', 'table', 'protocol',
209
- 'fwmark', 'fwmask', 'action'}
210
- or type(row.get('priority')) is not int or row.get('src') != 'all'):
211
- raise InventoryFailure('host_rules_unsupported')
211
+ if not isinstance(row, dict) or set(row) - {'priority', 'src', 'table', 'protocol',
212
+ 'fwmark', 'fwmask', 'action'}:
213
+ raise InventoryFailure('host_rules_unsupported_fields')
214
+ if type(row.get('priority')) is not int:
215
+ raise InventoryFailure('host_rules_unsupported_priority')
216
+ if row.get('src') != 'all':
217
+ raise InventoryFailure('host_rules_unsupported_source')
212
218
  normalized = {key: value for key, value in row.items() if key != 'protocol'}
213
219
  table = normalized.get('table')
214
220
  if table is not None:
215
221
  if type(table) not in (str, int):
216
- raise InventoryFailure('host_rules_unsupported')
222
+ raise InventoryFailure('host_rules_unsupported_table')
217
223
  normalized['table'] = tables.get(table, table)
218
- if normalized not in expected or normalized in actual:
219
- raise InventoryFailure('host_rules_unsupported')
224
+ wanted = next((item for item in expected if item['priority'] == normalized['priority']), None)
225
+ if wanted is None:
226
+ raise InventoryFailure('host_rules_unsupported_priority')
227
+ if set(normalized) != set(wanted):
228
+ raise InventoryFailure('host_rules_unsupported_structure')
229
+ for field in ('table', 'fwmark', 'fwmask', 'action'):
230
+ if normalized.get(field) != wanted.get(field):
231
+ raise InventoryFailure('host_rules_unsupported_' + field)
232
+ if normalized in actual:
233
+ raise InventoryFailure('host_rules_unsupported_duplicate')
220
234
  actual.append(normalized)
221
235
  if len(actual) != len(expected):
222
- raise InventoryFailure('host_rules_unsupported')
236
+ raise InventoryFailure('host_rules_unsupported_count')
223
237
 
224
238
 
225
239
  def resolver_exclusions(raw, *, resolved=False, records=None):
@@ -0,0 +1,69 @@
1
+ """Classify refused policy shapes without publishing their private values or relaxing admission."""
2
+ import copy
3
+ import json
4
+ import os
5
+ import unittest
6
+ from unittest.mock import patch
7
+
8
+ from network_diagnostics import SCHEMA, decode
9
+ from network_inventory import collect
10
+ from network_pools import InventoryFailure, validate_rules
11
+ from test_network_inventory import FakeHost, RULES
12
+
13
+
14
+ class RuleDiagnosticTests(unittest.TestCase):
15
+ def test_iproute_decimal_table_strings_keep_exact_policy_semantics(self):
16
+ marked = {'src': 'all', 'fwmark': '0x80000', 'fwmask': '0xff0000'}
17
+ rules = [dict(row, table=str({'local': 255, 'main': 254, 'default': 253}[row['table']]))
18
+ for row in RULES]
19
+ validate_rules(rules)
20
+ rules += [dict(marked, priority=5210, table='254'), dict(marked, priority=5230, table='253'),
21
+ dict(marked, priority=5250, action='unreachable'),
22
+ {'priority': 5270, 'src': 'all', 'table': '52'}]
23
+ validate_rules(rules)
24
+ for invalid in ('53', '052', '0x34', 'private table alias', 53, True):
25
+ changed = copy.deepcopy(rules)
26
+ changed[-1]['table'] = invalid
27
+ with self.subTest(table=invalid), self.assertRaises(InventoryFailure) as caught:
28
+ validate_rules(changed)
29
+ self.assertEqual(caught.exception.code, 'host_rules_unsupported_table')
30
+
31
+ def test_each_rejected_shape_keeps_a_closed_reason_without_values(self):
32
+ marked = {'src': 'all', 'fwmark': '0x80000', 'fwmask': '0xff0000'}
33
+ valid = RULES + [dict(marked, priority=5210, table='main'), dict(marked, priority=5230, table='default'),
34
+ dict(marked, priority=5250, action='unreachable'),
35
+ {'priority': 5270, 'src': 'all', 'table': 52}]
36
+ validate_rules(valid)
37
+ cases = [('fields', 'PRIVATE_SENTINEL', 'PRIVATE_SENTINEL'), ('priority', 'priority', True),
38
+ ('source', 'src', 'PRIVATE_SENTINEL'), ('table', 'table', 'PRIVATE_SENTINEL'),
39
+ ('fwmark', 'fwmark', 'PRIVATE_SENTINEL'), ('fwmask', 'fwmask', 'PRIVATE_SENTINEL')]
40
+ for reason, key, value in cases:
41
+ rows = copy.deepcopy(valid)
42
+ rows[3][key] = value
43
+ with self.subTest(reason=reason), self.assertRaises(InventoryFailure) as caught:
44
+ validate_rules(rows)
45
+ self.assertEqual(caught.exception.code, 'host_rules_unsupported_' + reason)
46
+ self.assertNotIn('PRIVATE_SENTINEL', str(caught.exception))
47
+ for rows, reason in ((valid[:-1], 'count'), (valid[:-1] + [valid[0]], 'duplicate'),
48
+ ([dict(row, fwmark='0x80000') for row in RULES], 'structure')):
49
+ with self.subTest(reason=reason), self.assertRaises(InventoryFailure) as caught:
50
+ validate_rules(rows)
51
+ self.assertEqual(caught.exception.code, 'host_rules_unsupported_' + reason)
52
+
53
+ def test_scoped_diagnostic_retains_reason_and_partial_metadata_only(self):
54
+ host = FakeHost()
55
+ host.answers[('ip', '-j', '-4', 'rule', 'show')][0]['src'] = 'PRIVATE_SENTINEL'
56
+ with patch.dict(os.environ, {}, clear=True), self.assertRaises(InventoryFailure) as caught:
57
+ collect(host, require_pools=False)
58
+ failure = caught.exception
59
+ result = dict(failure.details, schema=SCHEMA, ok=False, project='fixture', failure=failure.code)
60
+ raw = json.dumps(result).encode()
61
+ self.assertEqual(decode(1, raw, 'fixture')['failure'], 'host_rules_unsupported_source')
62
+ self.assertFalse(result['complete'])
63
+ self.assertNotIn(b'PRIVATE_SENTINEL', raw)
64
+ self.assertNotIn('networks', result)
65
+ self.assertFalse(any('create' in call or 'rm' in call for call in host.calls))
66
+
67
+
68
+ if __name__ == '__main__':
69
+ unittest.main()
@@ -79,6 +79,12 @@ A routing refusal can retain safe engine/pool metadata with `complete:false` and
79
79
  partial evidence never admits allocation. A successful diagnostic is a readback, not a deployment,
80
80
  capacity reservation, proof of application health or proof that another project cannot reach the app.
81
81
  Raw container data, labels, environment, daemon configuration and command errors are not exported.
82
+ Policy-rule refusals use `host_rules_unsupported_<reason>` to distinguish count, fields, priority,
83
+ source, table, structure, fwmark, fwmask, action and duplicate mismatches. These fixed classifications
84
+ retain the strict supported-rule comparison and publish no rejected keys or values.
85
+ The exact decimal-string spellings of supported table IDs are normalized because
86
+ [iproute2 prints rule tables as JSON strings](https://github.com/iproute2/iproute2/blob/v6.8.0/ip/iprule.c#L370).
87
+ This does not admit additional routing tables, aliases, priorities or forwarding policies.
82
88
 
83
89
  The deployment emits its own `gowalk-cicd/backend-networks.v1` receipt. The existing
84
90
  `backend_deploy_failed` phase remains `compose_up` on a preparation refusal. Normal public health,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.151",
3
+ "version": "1.0.153",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {