@gotcos/glasses-server 6.55.0 → 6.56.1

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/bin/control2-foundation.cjs +12 -0
  3. package/bin/task-runtime-check.cjs +65 -0
  4. package/docs/control2-foundation.md +79 -0
  5. package/docs/control2-sandbox.md +47 -0
  6. package/docs/control2-task-runtime.md +68 -0
  7. package/docs/work-reviews.md +25 -0
  8. package/docs/work-task-runtime.md +82 -0
  9. package/package.json +18 -4
  10. package/server/index.ts +8 -0
  11. package/server/lib/control2-draft.ts +118 -0
  12. package/server/lib/control2-foundation.ts +164 -0
  13. package/server/lib/control2-sandbox.ts +192 -0
  14. package/server/lib/cos-operations-meetings.ts +7 -0
  15. package/server/lib/provider-binary.ts +6 -2
  16. package/server/lib/task-bridge.ts +85 -0
  17. package/server/lib/task-store.ts +75 -10
  18. package/server/lib/work-activity.ts +106 -0
  19. package/server/lib/work-review-backend.ts +28 -0
  20. package/server/lib/work-review-runtime.ts +203 -0
  21. package/server/lib/work-review-store.ts +102 -0
  22. package/server/routes/control2-foundation.ts +56 -0
  23. package/server/routes/health.ts +4 -3
  24. package/server/routes/meetings.ts +24 -59
  25. package/server/routes/work-board.ts +79 -0
  26. package/server/routes/work-reviews.ts +24 -0
  27. package/server/scripts/control2-draft-canary.ts +18 -0
  28. package/server/scripts/control2-foundation-lab.ts +45 -0
  29. package/server/scripts/control2-sandbox-canary.ts +4 -0
  30. package/task-runtime/LICENSE +21 -0
  31. package/task-runtime/manifest.json +37 -0
  32. package/task-runtime/task_runtime.py +380 -0
  33. package/work-task-runtime/cos_atomic.py +90 -0
  34. package/work-task-runtime/manifest.json +33 -0
  35. package/work-task-runtime/runtime_io.py +27 -0
  36. package/work-task-runtime/task_bridge.py +87 -0
  37. package/work-task-runtime/task_checkout.py +380 -0
  38. package/work-task-runtime/task_commands.py +275 -0
  39. package/work-task-runtime/task_dedup.py +11 -0
  40. package/work-task-runtime/task_rows.py +420 -0
  41. package/work-task-runtime/task_work_metadata.py +107 -0
  42. package/work-task-runtime/task_write.py +645 -0
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env python3
2
+ """Portable read-only task protocol candidate. No production dispatch or writes.
3
+
4
+ Parser adapted from COS task_rows.py at MU snapshot 1dac83bd; see manifest.
5
+ """
6
+ from __future__ import annotations
7
+ import argparse
8
+ import hashlib
9
+ import json
10
+ import os
11
+ import re
12
+ import stat
13
+ import string
14
+ import sys
15
+ from dataclasses import asdict, dataclass
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ PROTOCOL = "cos-control-task-read/1"
20
+ VERSION = "0.1.0"
21
+ MAX_BYTES = 8 * 1024 * 1024
22
+
23
+ _MARKER_BODY = (
24
+ r"\[(?:run \d{4}-\d{2}-\d{2} \d{2}:\d{2}|agent running|"
25
+ r"agent #\d+ (?:done|failed)|agent failed|"
26
+ r"stage (?:planning|active|review))\]"
27
+ )
28
+ MARKER_RE = re.compile(r"\s*(?:" + _MARKER_BODY + r"\s*)+$")
29
+ MARKER_ANYWHERE_RE = re.compile(_MARKER_BODY)
30
+ # Workflow stage. An UNMARKED row is planning, so the 80 open rows need no
31
+ # migration. Lives in the line, not a sidecar: the task id is a hash of the
32
+ # description, so any id-keyed store orphans itself on the first text edit.
33
+ # NOTE: the trailing \s* sits INSIDE the repeat group. The writer joins markers
34
+ # with spaces (_rebuild_line), but the old regex allowed none between groups, so
35
+ # "[run ...] [agent running]" stripped only the last one and silently dropped the
36
+ # run time on every read. Pre-existing; stage makes that pairing the common case.
37
+ # STAGE_RE only reads a value out of a blob MARKER_RE has already validated, so
38
+ # its own alternation was a second, unreachable copy: mutating it changed nothing
39
+ # under test BECAUSE MARKER_RE's `$`-anchored match rejects "[stage bogus]" first.
40
+ # That is true of the READ path only. The WRITE path is gated separately by
41
+ # STAGES below (task_write.set_stage) and by cos_api_bridge's own literal tuple,
42
+ # so there are three stage vocabularies in this system, not one. Widening the set
43
+ # means editing MARKER_RE, STAGES, and the bridge tuple together.
44
+ STAGE_RE = re.compile(r"\[stage (\w+)\]")
45
+ STAGES = ("planning", "active", "review")
46
+ RUN_AT_RE = re.compile(r"\[run (\d{4}-\d{2}-\d{2} \d{2}:\d{2})\]")
47
+ AGENT_NUM_RE = re.compile(r"\[agent #(\d+) (done|failed)\]")
48
+ INBOX_RE = re.compile(r"^#{1,3}\s*INBOX\s*$", re.IGNORECASE)
49
+ TODAY_RE = re.compile(r"^#{1,3}\s*TODAY\s+(\d{4}-\d{2}-\d{2})\s*$", re.IGNORECASE)
50
+ DELEGATED_HEADING_RE = re.compile(r"^#{1,2}\s*Delegated\b", re.IGNORECASE)
51
+ BOLD_OWNER_RE = re.compile(r"^\*\*(?P<body>(?!Owner:)[^*]+)\*\*:\s*")
52
+ BRACKET_OWNER_RE = re.compile(r"^\[(?P<body>[^\]]+)\]\s+")
53
+ CHECK_RE = re.compile(r"^- \[([ xX~])\]\s*(.*)$")
54
+ PRIORITY_SECTIONS = frozenset(
55
+ {"inbox", "today", "urgent", "this_week", "backlog", "completed"}
56
+ )
57
+
58
+
59
+ @dataclass
60
+ class TaskRow:
61
+ ref: str
62
+ id: str
63
+ domain: str
64
+ description: str
65
+ priority: str
66
+ is_checked: bool
67
+ archived: bool
68
+ line_number: int
69
+ source: Optional[str]
70
+ owner: Optional[str]
71
+ delegated: bool
72
+ needs_review: bool
73
+ thread: None
74
+ run_at: Optional[str]
75
+ agent_state: Optional[str]
76
+ agent_no: Optional[int]
77
+ section: str
78
+ section_day: Optional[str]
79
+ stage: Optional[str] = None
80
+ done_when: Optional[str] = None
81
+
82
+
83
+ def split_source(line: str) -> tuple[str, Optional[str]]:
84
+ if "**Source:**" not in line:
85
+ return line.strip(), None
86
+ left, right = line.split("**Source:**", 1)
87
+ return left.strip().rstrip("—").strip(), right.strip()
88
+
89
+
90
+ DONE_WHEN_LABEL = "**Done when:**"
91
+
92
+
93
+ def split_done_when(text):
94
+ """Peel a **Done when:** segment off a description or source part.
95
+
96
+ Same convention as **Source:** so tasks.md stays readable by hand, and the
97
+ finish line travels with the row when the text is edited.
98
+ """
99
+ if not text or DONE_WHEN_LABEL not in text:
100
+ return text, None
101
+ left, right = text.split(DONE_WHEN_LABEL, 1)
102
+ return left.strip().rstrip("\u2014").strip(), (right.strip() or None)
103
+
104
+
105
+ def strip_markers(text: str) -> tuple[str, dict]:
106
+ """Remove end-of-line agent/run markers. Returns (text, fields)."""
107
+ fields: dict = {"run_at": None, "agent_state": None, "agent_no": None, "stage": None}
108
+ if not text:
109
+ return text, fields
110
+ match = MARKER_RE.search(text)
111
+ if not match:
112
+ return text, fields
113
+ blob = match.group(0)
114
+ run = RUN_AT_RE.search(blob)
115
+ if run:
116
+ fields["run_at"] = run.group(1)
117
+ stage = STAGE_RE.search(blob)
118
+ if stage:
119
+ fields["stage"] = stage.group(1)
120
+ numbered = AGENT_NUM_RE.search(blob)
121
+ if numbered:
122
+ fields["agent_no"] = int(numbered.group(1))
123
+ fields["agent_state"] = numbered.group(2)
124
+ elif "[agent running]" in blob:
125
+ fields["agent_state"] = "running"
126
+ elif "[agent failed]" in blob:
127
+ fields["agent_state"] = "failed"
128
+ return text[: match.start()].rstrip(), fields
129
+
130
+
131
+ def _merge_marker_fields(primary: dict, secondary: dict) -> dict:
132
+ out = dict(primary)
133
+ for key, value in secondary.items():
134
+ if value is not None:
135
+ out[key] = value
136
+ return out
137
+
138
+
139
+ def classify_heading(stripped: str) -> tuple[Optional[str], Optional[str]]:
140
+ """Return (section, section_day) for a heading, else (None, None)."""
141
+ if not stripped.startswith("#"):
142
+ return None, None
143
+ inbox = INBOX_RE.match(stripped)
144
+ if inbox:
145
+ return "inbox", None
146
+ today = TODAY_RE.match(stripped)
147
+ if today:
148
+ return "today", today.group(1)
149
+ upper = stripped.upper()
150
+ if "🔴 URGENT" in stripped or "URGENT" in upper:
151
+ return "urgent", None
152
+ if "🟡 THIS WEEK" in stripped or "THIS WEEK" in upper:
153
+ return "this_week", None
154
+ if "🟢 BACKLOG" in stripped or "BACKLOG" in upper:
155
+ return "backlog", None
156
+ if "✅ COMPLETED" in stripped or "COMPLETED" in upper:
157
+ return "completed", None
158
+ return "other", None
159
+
160
+
161
+ def _heading_depth(stripped: str) -> int:
162
+ i = 0
163
+ while i < len(stripped) and stripped[i] == "#":
164
+ i += 1
165
+ return i
166
+
167
+
168
+ def parse_owner(text: str) -> tuple[Optional[str], str]:
169
+ bold = BOLD_OWNER_RE.match(text)
170
+ if bold:
171
+ return bold.group("body").strip(), text[bold.end():]
172
+ bracket = BRACKET_OWNER_RE.match(text)
173
+ if bracket:
174
+ return bracket.group("body").strip(), text[bracket.end():]
175
+ return None, text
176
+
177
+
178
+ def owner_tokens(owner: str) -> list[str]:
179
+ return [part.strip() for part in re.split(r"\s*\+\s*|/", owner) if part.strip()]
180
+
181
+
182
+ def owner_includes_operator(owner: Optional[str], operator: Optional[str]) -> bool:
183
+ if not owner or not operator:
184
+ return False
185
+ for token in owner_tokens(owner):
186
+ parts = token.split("(")[0].strip().split()
187
+ if parts and parts[0].casefold() == operator.casefold():
188
+ return True
189
+ return False
190
+
191
+
192
+ def ends_delegated_zone(stripped: str) -> bool:
193
+ if stripped.startswith("---"):
194
+ return True
195
+ return stripped.startswith("#") and _heading_depth(stripped) <= 2
196
+
197
+
198
+ def parse_text(text: str, domain: str, operator: Optional[str] = None) -> list[TaskRow]:
199
+ rows: list[TaskRow] = []
200
+ current_priority = "backlog"
201
+ current_section = "other"
202
+ current_day: Optional[str] = None
203
+ in_comment = False
204
+ delegated_zone = False
205
+ counter = 0
206
+
207
+ for line_number, line in enumerate(text.splitlines(), 1):
208
+ stripped = line.strip()
209
+ if "<!--" in stripped:
210
+ in_comment = True
211
+ if "-->" in stripped:
212
+ in_comment = False
213
+ continue
214
+ if in_comment:
215
+ continue
216
+
217
+ if ends_delegated_zone(stripped):
218
+ delegated_zone = False
219
+ if DELEGATED_HEADING_RE.match(stripped):
220
+ delegated_zone = True
221
+
222
+ heading, heading_day = classify_heading(stripped)
223
+ if heading is not None:
224
+ current_section = heading
225
+ if heading != "other":
226
+ current_priority = heading
227
+ current_day = heading_day
228
+ else:
229
+ current_day = None
230
+ continue
231
+
232
+ checked = CHECK_RE.match(line)
233
+ if not checked:
234
+ continue
235
+
236
+ mark, raw = checked.group(1), checked.group(2)
237
+ archived = mark == "~"
238
+ is_checked = mark.lower() == "x"
239
+ description, source = split_source(raw)
240
+ description, desc_markers = strip_markers(description)
241
+ if source is not None:
242
+ source, src_markers = strip_markers(source)
243
+ markers = _merge_marker_fields(desc_markers, src_markers)
244
+ else:
245
+ markers = desc_markers
246
+
247
+ description, dw_desc = split_done_when(description)
248
+ if source is not None:
249
+ source, dw_src = split_done_when(source)
250
+ else:
251
+ dw_src = None
252
+ done_when = dw_desc or dw_src
253
+
254
+ needs_review = "[REVIEW]" in description
255
+ if needs_review:
256
+ description = description.replace("[REVIEW]", "").strip()
257
+
258
+ owner, description = parse_owner(description)
259
+ description = description.strip()
260
+ delegated = delegated_zone or (
261
+ owner is not None and not owner_includes_operator(owner, operator)
262
+ )
263
+ priority = "completed" if is_checked else (
264
+ current_section if current_section in PRIORITY_SECTIONS
265
+ else current_priority
266
+ )
267
+ counter += 1
268
+ rows.append(TaskRow(
269
+ ref=f"{domain}-{counter}",
270
+ id="",
271
+ domain=domain,
272
+ description=description,
273
+ priority=priority,
274
+ is_checked=is_checked,
275
+ archived=archived,
276
+ line_number=line_number,
277
+ source=source,
278
+ owner=owner,
279
+ delegated=delegated,
280
+ needs_review=needs_review,
281
+ thread=None,
282
+ run_at=markers["run_at"],
283
+ agent_state=markers["agent_state"],
284
+ agent_no=markers["agent_no"],
285
+ section=current_section,
286
+ section_day=current_day if current_section == "today" else None,
287
+ stage=markers["stage"],
288
+ done_when=done_when,
289
+ ))
290
+
291
+ _assign_ids(domain, rows)
292
+ return rows
293
+
294
+
295
+ def _assign_ids(domain: str, rows: list[TaskRow]) -> None:
296
+
297
+ seen: dict[str, int] = {}
298
+ for row in rows:
299
+ norm = normalize(row.description)
300
+ ordinal = seen.get(norm, 0)
301
+ seen[norm] = ordinal + 1
302
+ row.id = hashlib.sha256(
303
+ f"{domain}/tasks.md|{norm}|{ordinal}".encode()
304
+ ).hexdigest()[:12]
305
+
306
+
307
+ def normalize(text: str) -> str:
308
+ text, _ = strip_markers(text)
309
+ return " ".join(text.lower().translate(str.maketrans("", "", string.punctuation)).split())
310
+
311
+
312
+ def capabilities() -> dict:
313
+ return {"protocol": PROTOCOL, "version": VERSION, "commands": ["capabilities", "domains", "task-rows"],
314
+ "readOnly": True, "writes": False, "dispatch": False, "migration": False,
315
+ "identity": {"kind": "legacy-description-hash", "stable": False},
316
+ "limits": {"maxTaskFileBytes": MAX_BYTES}, "requiresPrivateCheckout": False}
317
+
318
+
319
+ def read_domain(root_fd: int, domain: str) -> str:
320
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,127}", domain):
321
+ raise ValueError("invalid_domain")
322
+ dfd = os.open(domain, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root_fd)
323
+ try:
324
+ fd = os.open("tasks.md", os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=dfd)
325
+ try:
326
+ before = os.fstat(fd)
327
+ if not stat.S_ISREG(before.st_mode): raise ValueError("not_regular_file")
328
+ if before.st_size > MAX_BYTES: raise ValueError("task_file_too_large")
329
+ with os.fdopen(fd, "rb", closefd=False) as stream:
330
+ data = stream.read(MAX_BYTES + 1)
331
+ after = os.fstat(fd)
332
+ if len(data) > MAX_BYTES: raise ValueError("task_file_too_large")
333
+ if (before.st_size, before.st_mtime_ns) != (after.st_size, after.st_mtime_ns):
334
+ raise ValueError("source_changed_retry")
335
+ return data.decode("utf-8")
336
+ finally: os.close(fd)
337
+ finally: os.close(dfd)
338
+
339
+
340
+ def main() -> int:
341
+ parser = argparse.ArgumentParser()
342
+ parser.add_argument("command", choices=["capabilities", "domains", "task-rows"])
343
+ parser.add_argument("--root")
344
+ parser.add_argument("--domain")
345
+ parser.add_argument("--operator", help="Optional first-name owner match; absence conservatively marks named owners delegated")
346
+ args = parser.parse_args()
347
+ try:
348
+ if args.command == "capabilities":
349
+ out = capabilities()
350
+ else:
351
+ if not args.root or not Path(args.root).is_absolute(): raise ValueError("absolute_root_required")
352
+ # Explicit root only. No env fallback, home discovery or private checkout.
353
+ root_fd = os.open(args.root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
354
+ try:
355
+ if args.command == "domains":
356
+ domains = []
357
+ for domain in sorted(os.listdir(root_fd)):
358
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,127}", domain): continue
359
+ try: read_domain(root_fd, domain)
360
+ except (OSError, ValueError, UnicodeError): continue
361
+ domains.append(domain)
362
+ out = {"protocol": PROTOCOL, "domains": domains}
363
+ else:
364
+ if not args.domain: raise ValueError("domain_required")
365
+ source = read_domain(root_fd, args.domain)
366
+ out = {"protocol": PROTOCOL, "domain": args.domain,
367
+ "sourceRevision": hashlib.sha256(source.encode("utf-8")).hexdigest(),
368
+ "identity": capabilities()["identity"],
369
+ "rows": [asdict(row) for row in parse_text(source, args.domain, args.operator)]}
370
+ finally: os.close(root_fd)
371
+ print(json.dumps(out, ensure_ascii=False))
372
+ return 0
373
+ except (OSError, ValueError, UnicodeError) as error:
374
+ code = str(error) if isinstance(error, ValueError) and not isinstance(error, UnicodeError) else "source_unavailable"
375
+ print(json.dumps({"protocol": PROTOCOL, "error": {"code": code}}))
376
+ return 1
377
+
378
+
379
+ if __name__ == "__main__":
380
+ raise SystemExit(main())
@@ -0,0 +1,90 @@
1
+ """Atomic file write helpers.
2
+
3
+ All COS state files that use JSON/JSONL must write atomically: write to a
4
+ temp sibling, then `os.replace` into place. A torn write (truncated file
5
+ after an open-write-close that crashed mid-way) is the Jan-31 incident
6
+ class — vector_state.json lost 744 entries because a crash hit between
7
+ truncate and final write.
8
+
9
+ Use `atomic_write_text` for text/JSON payloads and `atomic_write_json` as
10
+ a thin convenience wrapper. Both hold no lock — callers that need
11
+ cross-process serialization must layer their own locking around the call
12
+ (see lightrag_indexer._queue_lock for an example).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import uuid
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+
24
+ def atomic_write_text(
25
+ path: Path,
26
+ text: str,
27
+ encoding: str = "utf-8",
28
+ *,
29
+ durable: bool = False,
30
+ ) -> None:
31
+ """Write `text` to `path` via tmp + os.replace.
32
+
33
+ On POSIX, `os.replace` is atomic within a filesystem — the destination
34
+ either contains the old content or the new, never a torn in-between.
35
+
36
+ The tmp filename is suffixed with pid+uuid so concurrent callers on
37
+ the same path do not clobber each other's tmp content before replace.
38
+ If the write raises, the orphan tmp is best-effort deleted so failed
39
+ runs do not leave litter behind.
40
+
41
+ `durable=True` fsyncs the tmp fd before replace and the parent directory
42
+ after. Default stays the historical no-fsync path.
43
+ """
44
+ tmp = path.with_suffix(f"{path.suffix}.tmp.{os.getpid()}.{uuid.uuid4().hex[:8]}")
45
+ try:
46
+ data = text.encode(encoding)
47
+ if durable:
48
+ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o644)
49
+ try:
50
+ offset = 0
51
+ while offset < len(data):
52
+ try:
53
+ written = os.write(fd, data[offset:])
54
+ except InterruptedError:
55
+ continue
56
+ if written <= 0:
57
+ raise OSError("atomic write made no progress")
58
+ offset += written
59
+ os.fsync(fd)
60
+ finally:
61
+ os.close(fd)
62
+ else:
63
+ tmp.write_text(text, encoding=encoding)
64
+ os.replace(tmp, path)
65
+ if durable:
66
+ dir_fd = os.open(str(path.parent), os.O_RDONLY)
67
+ try:
68
+ os.fsync(dir_fd)
69
+ finally:
70
+ os.close(dir_fd)
71
+ except Exception:
72
+ try:
73
+ tmp.unlink(missing_ok=True)
74
+ except OSError:
75
+ pass
76
+ raise
77
+
78
+
79
+ def atomic_write_json(
80
+ path: Path,
81
+ data: Any,
82
+ indent: int | None = 2,
83
+ encoding: str = "utf-8",
84
+ *,
85
+ durable: bool = False,
86
+ ) -> None:
87
+ """Serialize `data` to JSON and write atomically."""
88
+ atomic_write_text(
89
+ path, json.dumps(data, indent=indent), encoding=encoding, durable=durable
90
+ )
@@ -0,0 +1,33 @@
1
+ {
2
+ "version": 1,
3
+ "protocol": "cos-control-task-write/1",
4
+ "pythonMinimum": "3.10",
5
+ "sourceCommit": "177d0bd875f446f5e3d57de52983d5ab4e679915",
6
+ "sourceHashes": {
7
+ "task_rows.py": "58b568fd4081072eddc3110b4c3397f5e5048ede9e052391e1d09f919e8c2e7d",
8
+ "task_write.py": "4d1d4c51cf5ec6fa75f941c2ce1aee05952fde9a6bac89be4fca51b0ffd37981",
9
+ "task_work_metadata.py": "534c45910e2109ac1e18eeb236c6b286d8a899f96c7a153d0187aee5839854a5",
10
+ "task_checkout.py": "56c94c5b28282398abc1f8daea9d910e64228afb8ac8cfae89b54117a7e30b86",
11
+ "cos_atomic.py": "055f7d6bc3093aeec7b051f87cb68c543637603a538f383a614c1903bd1b503e",
12
+ "cos_api_bridge.py": "1e17960332e8b778c8e5923de355f4395c9244692cd4d988ccd944c6a30575ef",
13
+ "task_dedup.py": "5331fdea01c293ecd2773726acea110e9339ff26e72e5bcdce66777ab518bfe8"
14
+ },
15
+ "adaptations": [
16
+ "Only canonical task command handlers extracted; no private bridge imports",
17
+ "Exact canonical normalization extracted without LLM dependencies",
18
+ "Generic configured operator replaces author-specific owner match",
19
+ "Bounded no-follow source reads and lock opens",
20
+ "Descriptor-pinned domain working directory and lock opens supplied by task_bridge.py"
21
+ ],
22
+ "files": {
23
+ "cos_atomic.py": "0da53eecaf07331b008c10b1e06c39701ecc3291c511fd0779e35795b3b22534",
24
+ "runtime_io.py": "e5b191b6af3a4966f517ecd05d3dcae9f4136a9e99cf36d7102303ca55ae928d",
25
+ "task_bridge.py": "8ec1f6533710c3de2c51e518586d2f6b120f90f7c3773480a08d53544ac07fd4",
26
+ "task_checkout.py": "669af045b2f64b9879df03aacae7a4550b46291974ed2df2ba2ed0775c1be78f",
27
+ "task_commands.py": "7e843b9164112d610190385df8e41d6044d6546d4ece92d09a60b6e4ad33cc75",
28
+ "task_dedup.py": "745971722898a3de520f8bb5246d714c70f3189dbe53bf8f42feafaaf241a51b",
29
+ "task_rows.py": "9e48375d9d6ee5d29bf3cc1f92bdf1182a6eea40ee305ff1fe1632b9f3a9fcf7",
30
+ "task_work_metadata.py": "534c45910e2109ac1e18eeb236c6b286d8a899f96c7a153d0187aee5839854a5",
31
+ "task_write.py": "238b869fac49c8e01bca72a1f28ffab74c7f94938e2323246936aec0241bbd16"
32
+ }
33
+ }
@@ -0,0 +1,27 @@
1
+ """Bounded, no-follow source I/O used by the portable canonical parser/writer."""
2
+ import os, stat
3
+ MAX_SOURCE=8*1024*1024
4
+
5
+ def safe_read_text(path):
6
+ fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK)
7
+ try:
8
+ before=os.fstat(fd)
9
+ if not stat.S_ISREG(before.st_mode) or before.st_size>MAX_SOURCE: raise ValueError('Unsafe or oversized task source')
10
+ parts=[]; count=0
11
+ while count<=MAX_SOURCE:
12
+ chunk=os.read(fd,min(65536,MAX_SOURCE+1-count))
13
+ if not chunk: break
14
+ parts.append(chunk); count+=len(chunk)
15
+ after=os.fstat(fd)
16
+ if count>MAX_SOURCE or count!=before.st_size or (before.st_size,before.st_mtime_ns)!=(after.st_size,after.st_mtime_ns): raise ValueError('Task source changed while reading')
17
+ return b''.join(parts).decode('utf-8')
18
+ finally: os.close(fd)
19
+
20
+ LOCK_FD=None
21
+ def open_lock(path, flags, mode=0o600):
22
+ if LOCK_FD is None: raise ValueError('No pinned lock directory')
23
+ fd=os.open(path.name,flags|os.O_NOFOLLOW|os.O_NONBLOCK,mode,dir_fd=LOCK_FD)
24
+ info=os.fstat(fd)
25
+ if not stat.S_ISREG(info.st_mode) or info.st_size>1048576:
26
+ os.close(fd); raise ValueError('Unsafe lock store')
27
+ return fd
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env python3
2
+ """Portable bridge over canonical tasks.md; no provider invocation or second store."""
3
+ import argparse, io, json, os, stat, sys
4
+ from pathlib import Path
5
+ # -I excludes cwd/site configuration. Add only our installed, hash-verified code.
6
+ sys.path.insert(0,str(Path(__file__).resolve().parent))
7
+ from runtime_io import safe_read_text
8
+
9
+ def safe_name(name):
10
+ return isinstance(name,str) and 0<len(name)<=64 and name==name.strip() and not name.startswith('.') and not any(c in name for c in '/\\\0') and not any(ord(c)<32 or ord(c)==127 for c in name)
11
+
12
+ def directory(path, create=False):
13
+ path=Path(path)
14
+ if not path.is_absolute() or '..' in path.parts: raise ValueError('Absolute canonical root required')
15
+ fd=os.open('/',os.O_RDONLY|os.O_DIRECTORY)
16
+ try:
17
+ for part in path.parts[1:]:
18
+ if create:
19
+ try: os.mkdir(part,0o700,dir_fd=fd)
20
+ except FileExistsError: pass
21
+ child=os.open(part,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=fd)
22
+ os.close(fd);fd=child
23
+ return fd
24
+ except BaseException: os.close(fd);raise
25
+
26
+ def main():
27
+ if sys.version_info<(3,10): raise ValueError("Python 3.10 or later required")
28
+ parser=argparse.ArgumentParser();parser.add_argument('--root',required=True);parser.add_argument('--lock-store',required=True);parser.add_argument('--domains',required=True);parser.add_argument('--operator',default='');parser.add_argument('command');parser.add_argument('args',nargs=argparse.REMAINDER)
29
+ opts=parser.parse_args(); domains=json.loads(opts.domains)
30
+ if not isinstance(domains,list) or not domains or len(domains)>256 or not all(safe_name(d) for d in domains): raise ValueError('Invalid domain inventory')
31
+ raw=sys.stdin.buffer.read(32769)
32
+ if len(raw)>32768: raise ValueError('Task input exceeds supported limit')
33
+ sys.stdin=io.StringIO(raw.decode('utf-8'))
34
+ commands={'task-rows':'cmd_task_rows','task-capture':'cmd_task_capture','task-set-text':'cmd_task_set_text','task-set-run-at':'cmd_task_set_run_at','task-set-marker':'cmd_task_set_marker','task-set-stage':'cmd_task_set_stage','task-set-done-when':'cmd_task_set_done_when','task-move':'cmd_task_move','task-check':'cmd_task_check','task-set-work-stage':'cmd_task_work_write','task-link-meeting':'cmd_task_work_write'}
35
+ if opts.command not in commands and opts.command!='task-work-capabilities': raise ValueError('Unsupported task command')
36
+ rootfd=directory(opts.root,create=True); domainfds={}; lockfd=None
37
+ try:
38
+ # Descriptor pinning prevents ancestor rename/symlink replacement from
39
+ # redirecting canonical source reads or atomic replacement writes.
40
+ def domain_path(domain):
41
+ if domain not in domains or not safe_name(domain): raise ValueError('Unknown task domain')
42
+ if domain not in domainfds:
43
+ try: os.mkdir(domain,0o700,dir_fd=rootfd)
44
+ except FileExistsError: pass
45
+ domainfds[domain]=os.open(domain,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=rootfd)
46
+ os.fchdir(domainfds[domain])
47
+ return Path('tasks.md')
48
+ lock=Path(opts.lock_store)
49
+ if not lock.is_absolute() or not safe_name(lock.name.lstrip('.')): raise ValueError('Invalid lock path')
50
+ lockfd=directory(lock.parent,create=True)
51
+ lockpath=lock
52
+ import runtime_io
53
+ runtime_io.LOCK_FD=lockfd
54
+ try:
55
+ info=os.stat(lock.name,dir_fd=lockfd,follow_symlinks=False)
56
+ if not stat.S_ISREG(info.st_mode) or info.st_size>1048576: raise ValueError('Unsafe lock store')
57
+ except FileNotFoundError: pass
58
+ os.environ['COS_TASK_LOCK_STORE']=str(lockpath);os.environ['COS_TASK_OPERATOR']=opts.operator
59
+ import task_rows
60
+ task_rows.full_domains=lambda:tuple(domains);task_rows.domain_path=domain_path
61
+ # Canonical aliases should not re-route a custom domain named sr or hc.
62
+ task_rows.SHORT_TO_FULL={k:v for k,v in task_rows.SHORT_TO_FULL.items() if k not in domains}
63
+ import task_write
64
+ task_write.domain_path=domain_path
65
+ original_write=task_write._guarded_write
66
+ def guarded(domain,identity,path,text):
67
+ if len(text.encode())>8*1024*1024: raise ValueError('Task source exceeds supported limit')
68
+ original_write(domain,identity,path,text)
69
+ task_write._guarded_write=guarded
70
+ import task_commands
71
+ if opts.command=='task-work-capabilities': print(json.dumps({'version':1,'protocol':'cos-control-task-write/1'}));return
72
+ if not opts.args or opts.args[0] not in domains: raise ValueError('Unknown task domain')
73
+ path=domain_path(opts.args[0])
74
+ try: safe_read_text(path)
75
+ except FileNotFoundError: pass
76
+ fn=getattr(task_commands,commands[opts.command])
77
+ if opts.command=='task-link-meeting':fn(opts.args,linking=True)
78
+ else:fn(opts.args)
79
+ finally:
80
+ for fd in domainfds.values():os.close(fd)
81
+ if lockfd is not None:os.close(lockfd)
82
+ os.close(rootfd)
83
+
84
+ if __name__=='__main__':
85
+ try: main()
86
+ except Exception:
87
+ print(json.dumps({'error':{'code':'task_runtime_unavailable','message':'Task source or portable runtime is unavailable. Check configuration before retrying.'}}))