From 33c7b9a26073944a49fda5b2b6d4ad6001f34532 Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Wed, 23 Sep 2026 14:50:56 +0800 Subject: [PATCH] feat(agentctl): externalize governance policy to JSON Load configs/agent-governance.json from the main worktree with fail-closed POLICY_* codes, and keep test fixtures writing the same contract file. --- .doc/ops/CURRENT.md | 1 + .../2026-09-23-agent-governance-config.md | 45 +++++ configs/agent-governance.json | 34 ++++ scripts/agentctl.py | 161 ++++++++++++++++-- scripts/agentctl_w4.py | 9 +- scripts/tests/test_agent_worktree.py | 3 + scripts/tests/test_agentctl.py | 43 +++++ scripts/tests/test_agentctl_w4.py | 3 + scripts/tests/test_ops_guards.py | 2 + 9 files changed, 287 insertions(+), 14 deletions(-) create mode 100644 .doc/ops/short-term/2026-09-23-agent-governance-config.md create mode 100644 configs/agent-governance.json diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index fc364e95..b862568f 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -18,6 +18,7 @@ | owner | task | branch | worktree | mutex | status | |---|---|---|---|---|---| +| Cursor | agent-governance-config-2026-09-23 | feature/agent-governance-config-2026-09-23 | .worktrees/agent-governance-config-2026-09-23 | agent-governance-config | in_progress | | Cursor | coordination-db-daemon-impl-plan-2026-09-23 | feature/coordination-db-daemon-impl-plan-2026-09-23 | .worktrees/coordination-db-daemon-impl-plan-2026-09-23 | coordination-db-daemon-impl-plan | done | | Cursor | coordination-db-daemon-2026-09-23 | feature/coordination-db-daemon-2026-09-23 | .worktrees/coordination-db-daemon-2026-09-23 | coordination-db-daemon | done | | Cursor | integrate-pi-runner-response-size-r2-2026-09-23 | feature/integrate-pi-runner-response-size-2026-09-23 | .worktrees/integrate-pi-runner-response-size-2026-09-23 | git:dev | done | diff --git a/.doc/ops/short-term/2026-09-23-agent-governance-config.md b/.doc/ops/short-term/2026-09-23-agent-governance-config.md new file mode 100644 index 00000000..d8e3422f --- /dev/null +++ b/.doc/ops/short-term/2026-09-23-agent-governance-config.md @@ -0,0 +1,45 @@ +--- +title: "W1-U2:治理策略外部化为 agent-governance.json" +task_id: agent-governance-config-2026-09-23 +status: in_progress +created: 2026-09-23 +updated: 2026-09-23 +owner: Cursor +task_type: implementation +task_tier: single +policy_revision: "agent-governance-2026-09-21-r1 @ 1cb8c6f4b3379ef1136972ab36c5e120829d11f8" +branch: feature/agent-governance-config-2026-09-23 +worktree: .worktrees/agent-governance-config-2026-09-23 +base_dev_sha: "1cb8c6f4b3379ef1136972ab36c5e120829d11f8" +write_scope: ["scripts", "configs", ".doc"] +allowed_environments: [] +mutex: agent-governance-config +review_state: pending +integration_state: pending +deployment: not_required +cleanup: pending +cleanup_owner: "独立维护者" +cleanup_trigger: "合入 local_dev 且无进程占用后复查" +remote_sync: not_requested +--- + +# W1-U2:治理策略外部化 + +## 用户目标与授权 + +- 用户目标:`configs/agent-governance.json`(stdlib JSON)外部化 SHARED_DOCS / JOB_KINDS / doc_rules 等;`load_policy` fail-closed。 +- 授权:W1 波次 U2;不 push。 +- 交付终点:合入 local_dev;负例三类拒绝码可测。 + +## 步骤 + +1. [x] worktree + register + CURRENT 认领 +2. [ ] 新建 `configs/agent-governance.json` +3. [ ] `load_policy` + 写路径前置校验;只读不创建 +4. [ ] 测试 fixture 写入策略文件 +5. [ ] 负例测试 + unittest 全绿 + integrate + +## 相关链接 + +- [实施承接计划](../../remediation/2026-09-23-coordination-db-daemon-implementation-undertaking-plan.md) +- 前序 U1r tip:`1cb8c6f4` diff --git a/configs/agent-governance.json b/configs/agent-governance.json new file mode 100644 index 00000000..07d9a621 --- /dev/null +++ b/configs/agent-governance.json @@ -0,0 +1,34 @@ +{ + "protocol_version": 1, + "min_protocol_version": 1, + "policy_revision": "agent-governance-2026-09-21-r1", + "shared_docs": [".doc/ops/CURRENT.md", ".doc/status/FEATURE-MATRIX.md"], + "projectable_docs": [".doc/ops/CURRENT.md", ".doc/status/FEATURE-MATRIX.md"], + "projectable_prefixes": [".doc/ops/", ".doc/status/", ".doc/remediation/"], + "job_kinds": [ + "validate", + "integrate", + "with", + "deploy_prepare", + "deploy_run", + "deploy_accept", + "docs_project", + "cleanup_inspect", + "noop" + ], + "doc_rules": { + ".doc/ops/CURRENT.md": { + "own_row_only": true, + "slot_protected": true, + "header": "| owner | task |" + }, + ".doc/status/FEATURE-MATRIX.md": { + "evidence_for_done": true, + "header": "| 模块 |" + } + }, + "reserved_record_ids": ["holder", "idle", "Cursor", "Codex", "-"], + "protected_sections": { + ".doc/ops/CURRENT.md": ["## 集成槽位"] + } +} diff --git a/scripts/agentctl.py b/scripts/agentctl.py index 29984226..3268d539 100755 --- a/scripts/agentctl.py +++ b/scripts/agentctl.py @@ -24,7 +24,36 @@ sys.path.insert(0, str(Path(__file__).resolve().parent)) import agentctl_w4 as w4 # noqa: E402 VERSION = 1 -SHARED_DOCS = {'.doc/ops/CURRENT.md', '.doc/status/FEATURE-MATRIX.md'} +POLICY_REL = 'configs/agent-governance.json' +DEFAULT_POLICY = { + 'protocol_version': 1, + 'min_protocol_version': 1, + 'policy_revision': 'agent-governance-2026-09-21-r1', + 'shared_docs': ['.doc/ops/CURRENT.md', '.doc/status/FEATURE-MATRIX.md'], + 'projectable_docs': ['.doc/ops/CURRENT.md', '.doc/status/FEATURE-MATRIX.md'], + 'projectable_prefixes': ['.doc/ops/', '.doc/status/', '.doc/remediation/'], + 'job_kinds': [ + 'validate', 'integrate', 'with', 'deploy_prepare', 'deploy_run', + 'deploy_accept', 'docs_project', 'cleanup_inspect', 'noop', + ], + 'doc_rules': { + '.doc/ops/CURRENT.md': { + 'own_row_only': True, + 'slot_protected': True, + 'header': '| owner | task |', + }, + '.doc/status/FEATURE-MATRIX.md': { + 'evidence_for_done': True, + 'header': '| 模块 |', + }, + }, + 'reserved_record_ids': ['holder', 'idle', 'Cursor', 'Codex', '-'], + 'protected_sections': {'.doc/ops/CURRENT.md': ['## 集成槽位']}, +} +SHARED_DOCS = set(DEFAULT_POLICY['shared_docs']) +RESERVED_RECORD_IDS = set(DEFAULT_POLICY['reserved_record_ids']) +DOC_RULES = dict(DEFAULT_POLICY['doc_rules']) +PROTECTED_SECTIONS = {k: list(v) for k, v in DEFAULT_POLICY['protected_sections'].items()} ACTIVE = ('running', 'recovery_required') TERMINAL = ('completed', 'failed', 'recovered') QUEUE_TERMINAL = ('completed', 'failed', 'cancelled') @@ -69,6 +98,100 @@ def digest(value): return hashlib.sha256(value.encode() if isinstance(value, str) else value).hexdigest() +def _policy_path(main: Path) -> Path: + return Path(main) / POLICY_REL + + +def _validate_policy_shape(data) -> dict: + if not isinstance(data, dict): + deny('POLICY_INVALID', 'Governance policy must be a JSON object') + required = ( + 'protocol_version', 'min_protocol_version', 'policy_revision', + 'shared_docs', 'projectable_docs', 'projectable_prefixes', 'job_kinds', + 'doc_rules', 'reserved_record_ids', 'protected_sections', + ) + missing = [key for key in required if key not in data] + if missing: + deny('POLICY_INVALID', 'Governance policy missing required keys', keys=missing) + if not isinstance(data['protocol_version'], int) or not isinstance(data['min_protocol_version'], int): + deny('POLICY_INVALID', 'protocol_version and min_protocol_version must be integers') + if data['protocol_version'] < data['min_protocol_version']: + deny('POLICY_MISMATCH', 'protocol_version below min_protocol_version', + protocol_version=data['protocol_version'], + min_protocol_version=data['min_protocol_version']) + for key in ('shared_docs', 'projectable_docs', 'projectable_prefixes', + 'job_kinds', 'reserved_record_ids'): + if not isinstance(data[key], list) or not all(isinstance(item, str) for item in data[key]): + deny('POLICY_INVALID', f'{key} must be a list of strings') + if not isinstance(data['doc_rules'], dict) or not isinstance(data['protected_sections'], dict): + deny('POLICY_INVALID', 'doc_rules and protected_sections must be objects') + if not isinstance(data['policy_revision'], str) or not data['policy_revision']: + deny('POLICY_INVALID', 'policy_revision must be a nonempty string') + # Structure must stay compatible with the builtin DEFAULT_POLICY contract. + if set(data['shared_docs']) != set(DEFAULT_POLICY['shared_docs']): + deny('POLICY_MISMATCH', 'shared_docs diverges from DEFAULT_POLICY contract') + if set(data['job_kinds']) != set(DEFAULT_POLICY['job_kinds']): + deny('POLICY_MISMATCH', 'job_kinds diverges from DEFAULT_POLICY contract') + if set(data['projectable_docs']) != set(DEFAULT_POLICY['projectable_docs']): + deny('POLICY_MISMATCH', 'projectable_docs diverges from DEFAULT_POLICY contract') + if list(data['projectable_prefixes']) != list(DEFAULT_POLICY['projectable_prefixes']): + deny('POLICY_MISMATCH', 'projectable_prefixes diverges from DEFAULT_POLICY contract') + return data + + +def apply_policy(policy: dict) -> dict: + """Install a validated policy into module globals used by write paths.""" + global SHARED_DOCS, RESERVED_RECORD_IDS, DOC_RULES, PROTECTED_SECTIONS + SHARED_DOCS = set(policy['shared_docs']) + RESERVED_RECORD_IDS = set(policy['reserved_record_ids']) + DOC_RULES = {key: dict(value) for key, value in policy['doc_rules'].items()} + PROTECTED_SECTIONS = {key: list(value) for key, value in policy['protected_sections'].items()} + w4.apply_policy(policy) + return policy + + +def load_policy(main: Path) -> dict: + """Load governance policy from the main worktree. Fail-closed; never creates the file.""" + path = _policy_path(main) + if not path.is_file(): + deny('POLICY_MISSING', f'Governance policy missing at {POLICY_REL}', path=str(path)) + try: + raw = path.read_text(encoding='utf-8') + data = json.loads(raw) + except (OSError, UnicodeError) as exc: + deny('POLICY_INVALID', f'Cannot read governance policy: {exc}', path=str(path)) + except json.JSONDecodeError as exc: + deny('POLICY_INVALID', f'Governance policy is not valid JSON: {exc}', path=str(path)) + policy = _validate_policy_shape(data) + return apply_policy(policy) + + +def policy_status(main: Path) -> dict: + """Read-only policy probe for status/doctor. Never creates files.""" + path = _policy_path(main) + if not path.is_file(): + return {'status': 'missing', 'code': 'POLICY_MISSING', 'path': str(path)} + try: + data = json.loads(path.read_text(encoding='utf-8')) + policy = _validate_policy_shape(data) + apply_policy(policy) + return { + 'status': 'ok', + 'path': str(path), + 'policy_revision': policy['policy_revision'], + 'protocol_version': policy['protocol_version'], + } + except Denied as exc: + return {'status': 'denied', 'code': exc.code, 'message': exc.message, 'path': str(path), + **exc.details} + except (OSError, UnicodeError) as exc: + return {'status': 'denied', 'code': 'POLICY_INVALID', 'message': str(exc), 'path': str(path)} + + +def require_policy(repo) -> dict: + return load_policy(repo.main) + + def git(path, *args, index=False, check=True, input=None): env = os.environ.copy() for key in GIT_CONTEXT: @@ -879,7 +1002,7 @@ def docs_apply(repo, args): deny('CAPABILITY_REQUIRED', 'Document layout changes require docs:admin') if filename.endswith('CURRENT.md') and record_id != args.task and 'docs:admin' not in json.loads(task['capabilities']): deny('PATH_OUT_OF_SCOPE', 'A task can update only its own CURRENT record') - if record_id in ('holder', 'idle', 'Cursor', 'Codex', '-'): + if record_id in RESERVED_RECORD_IDS: deny('PATH_OUT_OF_SCOPE', 'Use task ids; slot ownership is not a document edit') path = safe_path(repo.current, filename) if filename not in before: @@ -900,14 +1023,14 @@ def docs_apply(repo, args): deny('INVALID_REQUEST', 'Expected full document text') if any(other.get('file') == filename and other is not change for other in request['changes']): deny('INVALID_REQUEST', 'Whole-document operation cannot mix row edits for the same file') - if filename.endswith('CURRENT.md'): - def slot(text): - match = re.search(r'(?m)^## 集成槽位\s*\n(.*?)(?=^## |\Z)', text, re.S | re.M) + for section in PROTECTED_SECTIONS.get(filename, []): + def slot(text, marker=section): + match = re.search(r'(?m)^' + re.escape(marker) + r'\s*\n(.*?)(?=^## |\Z)', text, re.S | re.M) return match.group(1) if match else None if slot(replacement) != slot(before[filename]): deny('PATH_OUT_OF_SCOPE', 'Document admin cannot overwrite integration slot ownership') # A layout update cannot bypass the feature-completion evidence rule. - if filename.endswith('FEATURE-MATRIX.md'): + if DOC_RULES.get(filename, {}).get('evidence_for_done') or filename.endswith('FEATURE-MATRIX.md'): old_states = matrix_states(before[filename]) new_states = matrix_states(replacement) if any(value == 'done' and old_states.get(key) != 'done' for key,value in new_states.items()): @@ -931,7 +1054,9 @@ def docs_apply(repo, args): if live: lines[live[0][0]] = replacement else: - header = '| owner | task |' if filename.endswith('CURRENT.md') else '| 模块 |' + header = DOC_RULES.get(filename, {}).get('header') + if not header: + header = '| owner | task |' if filename.endswith('CURRENT.md') else '| 模块 |' positions = [i for i, line in enumerate(lines) if line.startswith(header)] if len(positions) != 1: deny('INVALID_DOCUMENT', 'Target table header is absent or ambiguous') @@ -1640,9 +1765,24 @@ def parser(): def main(): args=parser().parse_args();repo=Repo(args.repo) + write_actions = { + 'init', 'task', 'with', 'docs', 'validate', 'integrate', 'queue', + 'deploy', 'recover', 'freeze', 'unfreeze', 'hooks', 'guard', 'cleanup', + } + # status/doctor remain readable without a policy file and never create one. + if args.action in write_actions: + if args.action == 'docs' and getattr(args, 'doc_action', None) == 'snapshot': + pass # snapshot is read-only CAS helper + elif args.action == 'queue' and getattr(args, 'queue_action', None) == 'list': + pass + elif args.action == 'cleanup': + require_policy(repo) # status report still needs contract constants + else: + require_policy(repo) if args.action=='init':initialize(repo) elif args.action in ('status','doctor'): identity=repo.identity() + policy=policy_status(repo.main) try: with repo.db(True) as db: ensure_schema(db) @@ -1658,7 +1798,7 @@ def main(): queue=[] for row in runs:row['supervisor_alive']=alive_status(row['pid'],row['start'],row['boot']) emit({'status':'ok','lock_root':str(repo.root),'main':str(repo.main),'version':VERSION,'epoch':identity['epoch'], - 'tasks':tasks,'runs':runs,'queue':queue}) + 'tasks':tasks,'runs':runs,'queue':queue,'policy':policy}) elif args.action=='task': if args.task_action=='register': if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]{0,127}',args.id):deny('INVALID_TASK','Invalid task id') @@ -1733,11 +1873,6 @@ def main(): elif args.action=='validate':return validate(repo,args) elif args.action=='integrate':return integrate(repo,args) elif args.action=='deploy':return deploy_cmd(repo,args) - elif args.action=='queue': - if args.queue_action=='enqueue':return queue_enqueue(repo,args) - if args.queue_action=='list':return queue_list(repo,args) - if args.queue_action=='cancel':return queue_cancel(repo,args) - if args.queue_action=='schedule':return queue_schedule(repo,args) return 0 diff --git a/scripts/agentctl_w4.py b/scripts/agentctl_w4.py index 0b30d826..4eb1f3a3 100644 --- a/scripts/agentctl_w4.py +++ b/scripts/agentctl_w4.py @@ -75,11 +75,18 @@ DEPLOY_PHASES = ( PROJECTABLE_DOCS = frozenset({ '.doc/ops/CURRENT.md', '.doc/status/FEATURE-MATRIX.md', - '.doc/ops/CURRENT.md', # duplicate ok in set }) PROJECTABLE_PREFIXES = ('.doc/ops/', '.doc/status/', '.doc/remediation/') +def apply_policy(policy: dict) -> None: + """Apply governance policy fields that W4 owns (job kinds / projectable docs).""" + global JOB_KINDS, PROJECTABLE_DOCS, PROJECTABLE_PREFIXES + JOB_KINDS = frozenset(policy['job_kinds']) + PROJECTABLE_DOCS = frozenset(policy['projectable_docs']) + PROJECTABLE_PREFIXES = tuple(policy['projectable_prefixes']) + + def ensure_schema(db: sqlite3.Connection) -> None: db.executescript(SCHEMA_SQL) diff --git a/scripts/tests/test_agent_worktree.py b/scripts/tests/test_agent_worktree.py index 0abc2bbd..84d32ec1 100644 --- a/scripts/tests/test_agent_worktree.py +++ b/scripts/tests/test_agent_worktree.py @@ -26,6 +26,9 @@ class WorktreeTest(unittest.TestCase): (self.root / "scripts").mkdir() for name in ("agent-worktree.sh", "agent_worktree.py", "agentctl.py", "agentctl", "agentctl_w4.py"): shutil.copy2(SOURCE / name, self.root / "scripts" / name) + (self.root / "configs").mkdir(parents=True, exist_ok=True) + shutil.copy2(SOURCE.parent / "configs" / "agent-governance.json", + self.root / "configs" / "agent-governance.json") (self.root / ".gitignore").write_text(".worktrees/\n.locks/\nignored-evidence/\n") (self.root / "source.txt").write_text("base\n") self.env = {**os.environ, "PYTHONDONTWRITEBYTECODE": "1"} diff --git a/scripts/tests/test_agentctl.py b/scripts/tests/test_agentctl.py index b0a18b97..930cdbf5 100644 --- a/scripts/tests/test_agentctl.py +++ b/scripts/tests/test_agentctl.py @@ -16,6 +16,13 @@ import unittest SOURCE = pathlib.Path(__file__).resolve().parents[1] / "agentctl.py" +POLICY_SOURCE = SOURCE.parents[1] / "configs" / "agent-governance.json" + + +def install_governance_policy(root: pathlib.Path) -> None: + dest = root / "configs" + dest.mkdir(parents=True, exist_ok=True) + shutil.copy2(POLICY_SOURCE, dest / "agent-governance.json") class AgentctlTests(unittest.TestCase): @@ -30,6 +37,7 @@ class AgentctlTests(unittest.TestCase): (self.root / "scripts").mkdir() shutil.copy2(SOURCE, self.root / "scripts" / "agentctl.py") shutil.copy2(SOURCE.parent / "agentctl_w4.py", self.root / "scripts" / "agentctl_w4.py") + install_governance_policy(self.root) (self.root / ".gitignore").write_text(".locks/\n.worktrees/\n", encoding="utf-8") for directory in ("src/a", "src/b", ".doc/ops", ".doc/status"): (self.root / directory).mkdir(parents=True, exist_ok=True) @@ -747,6 +755,40 @@ class AgentctlTests(unittest.TestCase): item = next(i for i in listed["items"] if i["id"] == "q-sched-1") self.assertEqual(item["state"], "completed") + def test_policy_missing_rejects_write_but_status_stays_readonly(self): + self.cli("init") + policy = self.root / "configs" / "agent-governance.json" + policy.unlink() + self.assertFalse(policy.exists()) + denied = self.cli("hooks", "install", check=False) + self.assertNotEqual(denied.returncode, 0) + self.assertIn("POLICY_MISSING", denied.stderr) + self.assertFalse(policy.exists()) + status = json.loads(self.cli("status").stdout) + self.assertEqual(status["status"], "ok") + self.assertEqual(status["policy"]["code"], "POLICY_MISSING") + self.assertFalse(policy.exists()) + doctor = json.loads(self.cli("doctor").stdout) + self.assertEqual(doctor["status"], "ok") + self.assertEqual(doctor["policy"]["code"], "POLICY_MISSING") + self.assertFalse(policy.exists()) + + def test_policy_invalid_json_rejects_write(self): + policy = self.root / "configs" / "agent-governance.json" + policy.write_text("{not-json", encoding="utf-8") + denied = self.cli("init", check=False) + self.assertNotEqual(denied.returncode, 0) + self.assertIn("POLICY_INVALID", denied.stderr) + + def test_policy_protocol_too_low_rejects_write(self): + policy = self.root / "configs" / "agent-governance.json" + data = json.loads(policy.read_text(encoding="utf-8")) + data["protocol_version"] = 0 + policy.write_text(json.dumps(data), encoding="utf-8") + denied = self.cli("init", check=False) + self.assertNotEqual(denied.returncode, 0) + self.assertIn("POLICY_MISMATCH", denied.stderr) + class ProcessPlatformTests(unittest.TestCase): @classmethod @@ -818,6 +860,7 @@ class ProcessPlatformTests(unittest.TestCase): (root / "scripts").mkdir() shutil.copy2(SOURCE, root / "scripts" / "agentctl.py") shutil.copy2(SOURCE.parent / "agentctl_w4.py", root / "scripts" / "agentctl_w4.py") + install_governance_policy(root) (root / ".gitignore").write_text(".locks/\n.worktrees/\n", encoding="utf-8") (root / ".doc/ops").mkdir(parents=True) (root / ".doc/ops/CURRENT.md").write_text("# c\n", encoding="utf-8") diff --git a/scripts/tests/test_agentctl_w4.py b/scripts/tests/test_agentctl_w4.py index 1f53b70f..5624be7d 100644 --- a/scripts/tests/test_agentctl_w4.py +++ b/scripts/tests/test_agentctl_w4.py @@ -32,6 +32,9 @@ class AgentctlW4Tests(unittest.TestCase): self.root.mkdir() (self.root / "scripts").mkdir() shutil.copy2(W4_SOURCE, self.root / "scripts" / "agentctl_w4.py") + (self.root / "configs").mkdir(parents=True, exist_ok=True) + shutil.copy2(SOURCE.parent / "configs" / "agent-governance.json", + self.root / "configs" / "agent-governance.json") self.w4 = load_w4(self.root / "scripts" / "agentctl_w4.py") self.db_path = Path(self.tmp.name) / "w4.sqlite" self.db = sqlite3.connect(self.db_path) diff --git a/scripts/tests/test_ops_guards.py b/scripts/tests/test_ops_guards.py index c973c216..ec82474d 100644 --- a/scripts/tests/test_ops_guards.py +++ b/scripts/tests/test_ops_guards.py @@ -24,6 +24,8 @@ class OpsGuardsTest(unittest.TestCase): (self.root / ".gitignore").write_text(".worktrees/\n.locks/\nconfigs/.env.*\n!configs/.env.*.example\ndata/\n") (self.root / ".dockerignore").write_text(".doc\n.locks\n.worktrees\n") (self.root / "configs").mkdir() + policy_src = SOURCE.parent / "configs" / "agent-governance.json" + shutil.copy2(policy_src, self.root / "configs" / "agent-governance.json") for target in ("shared", "dev", "test", "main"): (self.root / f"docker-compose.{target}.yml").write_text("services: {}\n") (self.root / "configs" / f".env.{target}.example").write_text("EXAMPLE=1\n")