@etiquekit/etq 0.0.1 → 1.0.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 (83) hide show
  1. package/AGENTS.md +28 -0
  2. package/QuickStart.md +94 -0
  3. package/README.md +77 -9
  4. package/bin/etiquette +1149 -0
  5. package/bin/etiquette-core +313 -0
  6. package/docs/ARCHITECTURE.md +100 -0
  7. package/docs/CODEX_CLIENT_COMPATIBILITY.md +83 -0
  8. package/docs/CONCEPTS.md +128 -0
  9. package/docs/CONCEPT_STATUS.md +67 -0
  10. package/docs/CORE_PROFILE.md +70 -0
  11. package/docs/LANE_PROVISIONING.md +157 -0
  12. package/docs/README.md +29 -0
  13. package/docs/RELEASE_SURFACE_AUDIT.md +156 -0
  14. package/docs/SEAT_DISCIPLINE.md +68 -0
  15. package/docs/SEAT_PROVISIONING.md +100 -0
  16. package/docs/TEAM_HANDOFF.md +190 -0
  17. package/docs/WORKTREE_QOL.md +92 -0
  18. package/docs/contracts/ledger-entry/README.md +83 -0
  19. package/docs/contracts/ledger-entry/ledger-entry.v0.1.md +116 -0
  20. package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +90 -0
  21. package/docs/contracts/ledger-entry/ledger-entry.v0.md +123 -0
  22. package/docs/contracts/ledger-entry/ledger-query-cli.v0.1.md +64 -0
  23. package/package.json +42 -11
  24. package/scripts/install.sh +144 -0
  25. package/scripts/uninstall.sh +58 -0
  26. package/templates/etiquette-vanilla-v0/README.md +69 -0
  27. package/templates/etiquette-vanilla-v0/bin/stamp-vanilla +33 -0
  28. package/templates/etiquette-vanilla-v0/source/control-seat/.llm-substrate/HANDOFF.md +22 -0
  29. package/templates/etiquette-vanilla-v0/source/control-seat/README.md +16 -0
  30. package/templates/etiquette-vanilla-v0/source/control-seat/bin/_lib/http.sh +121 -0
  31. package/templates/etiquette-vanilla-v0/source/control-seat/bin/_lib/sanitize.sh +191 -0
  32. package/templates/etiquette-vanilla-v0/source/control-seat/bin/_lib/state.sh +67 -0
  33. package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +55 -0
  34. package/templates/etiquette-vanilla-v0/source/control-seat/bin/compact-local-events +71 -0
  35. package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/post-tool-use +64 -0
  36. package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/session-end +64 -0
  37. package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/session-start +64 -0
  38. package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/stop +64 -0
  39. package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/user-prompt-submit +64 -0
  40. package/templates/etiquette-vanilla-v0/source/control-seat/bin/local-event +53 -0
  41. package/templates/etiquette-vanilla-v0/source/control-seat/bin/post-receipt +143 -0
  42. package/templates/etiquette-vanilla-v0/source/control-seat/bin/post-telemetry +112 -0
  43. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-consume-pass +115 -0
  44. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +20 -0
  45. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-init +86 -0
  46. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-post-readiness +171 -0
  47. package/templates/etiquette-vanilla-v0/source/control-seat/bin/session-init +132 -0
  48. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +39 -0
  49. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +53 -0
  50. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/ledger/LEDGER.md +7 -0
  51. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/playbacks/PLAYBACK_TEMPLATE.md +31 -0
  52. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/receipts/.gitkeep +1 -0
  53. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/tasks/CONTROL-001.yaml +13 -0
  54. package/templates/etiquette-vanilla-v0/source/etiquette-kernel/README.md +6 -0
  55. package/templates/etiquette-vanilla-v0/source/etiquette-kernel/bin/kernel-doctor +11 -0
  56. package/templates/etiquette-vanilla-v0/source/etiquette-kernel/contracts/playback.v0.md +37 -0
  57. package/templates/etiquette-vanilla-v0/source/etiquette-kernel/contracts/receipt.v0.md +25 -0
  58. package/templates/etiquette-vanilla-v0/source/etiquette-kernel/contracts/seat-readiness.v0.md +22 -0
  59. package/templates/etiquette-vanilla-v0/source/etiquette-kernel/contracts/task.v0.md +18 -0
  60. package/templates/etiquette-vanilla-v0/validate-vanilla.sh +43 -0
  61. package/templates/github-actions/README.md +30 -0
  62. package/templates/github-actions/w1-webhook-wake-canary.yml +111 -0
  63. package/templates/hosted-receiver/README.md +41 -0
  64. package/templates/hosted-receiver/w1-github-webhook-receiver.mjs +129 -0
  65. package/templates/seat-packs-v0/README.md +72 -0
  66. package/templates/seat-packs-v0/bin/provision-seat +70 -0
  67. package/templates/seat-packs-v0/source/claude-code-seat/.llm-substrate/HANDOFF.md +24 -0
  68. package/templates/seat-packs-v0/source/claude-code-seat/README.md +19 -0
  69. package/templates/seat-packs-v0/source/claude-code-seat/bin/seat-doctor +8 -0
  70. package/templates/seat-packs-v0/source/codex-seat/.llm-substrate/HANDOFF.md +25 -0
  71. package/templates/seat-packs-v0/source/codex-seat/README.md +22 -0
  72. package/templates/seat-packs-v0/source/codex-seat/bin/seat-doctor +8 -0
  73. package/templates/seat-packs-v0/source/gemini-seat/.llm-substrate/HANDOFF.md +23 -0
  74. package/templates/seat-packs-v0/source/gemini-seat/README.md +19 -0
  75. package/templates/seat-packs-v0/source/gemini-seat/bin/seat-doctor +8 -0
  76. package/templates/seat-packs-v0/source/ollama-seat/.llm-substrate/HANDOFF.md +24 -0
  77. package/templates/seat-packs-v0/source/ollama-seat/README.md +19 -0
  78. package/templates/seat-packs-v0/source/ollama-seat/bin/seat-doctor +8 -0
  79. package/templates/seat-packs-v0/source/openrouter-seat/.llm-substrate/HANDOFF.md +24 -0
  80. package/templates/seat-packs-v0/source/openrouter-seat/README.md +19 -0
  81. package/templates/seat-packs-v0/source/openrouter-seat/bin/seat-doctor +8 -0
  82. package/templates/seat-packs-v0/validate-seat-packs.sh +27 -0
  83. package/bin/etq +0 -4
@@ -0,0 +1,69 @@
1
+ # Etiquette Vanilla V0
2
+
3
+ Minimal default onboarding kit.
4
+
5
+ It creates:
6
+
7
+ ```yaml
8
+ default_surface:
9
+ - etiquette-kernel
10
+ - control-seat
11
+ ```
12
+
13
+ It does not create runtime seats, product repos, backplanes, hosted services, dashboards, or sample products.
14
+ It starts unauthenticated: `workspace-secure-profile.v0` is `open`, with upgrade paths for `screening-warn`, `screening-enforced`, and `enterprise-reactor`.
15
+
16
+ ## Quick Start
17
+
18
+ From this template directory:
19
+
20
+ ```sh
21
+ sh bin/stamp-vanilla /tmp/etiquette-start
22
+ sh /tmp/etiquette-start/validate-vanilla.sh
23
+ ```
24
+
25
+ The first person who claims the stamped `control-seat` reports readiness before assigning work.
26
+ For restricted seats, change the secure profile, add provider-derived policy/claims, then run `sh source/control-seat/bin/access-assurance-check`.
27
+
28
+ ## Local Receipt Proof
29
+
30
+ The control seat can prove local-only compaction without any org service:
31
+
32
+ ```sh
33
+ sh source/control-seat/bin/local-event CONTROL-001 control-seat started "started local proof"
34
+ sh source/control-seat/bin/local-event CONTROL-001 control-seat completed "ready to compact"
35
+ sh source/control-seat/bin/compact-local-events CONTROL-001 success
36
+ ```
37
+
38
+ This writes a compact receipt under `docs/work/receipts/`.
39
+
40
+ ## Manual Playback
41
+
42
+ When workflow changes materially, fill `docs/work/playbacks/PLAYBACK_TEMPLATE.md`.
43
+ Playback interprets receipts; it does not replace them.
44
+
45
+ ## Control Seat
46
+
47
+ ```yaml
48
+ starts_as: generic
49
+ can:
50
+ - initialize local workflow
51
+ - report readiness
52
+ - create the first task
53
+ - grant another seat only when needed
54
+ cannot:
55
+ - clone product repos before assignment
56
+ - bypass validation
57
+ - treat chat as evidence
58
+ - grant access from the secure profile or reactor
59
+ ```
60
+
61
+ Provision these later only after the control seat grants them:
62
+
63
+ ```sh
64
+ sh templates/seat-packs-v0/bin/provision-seat codex /tmp/dev-codex-seat
65
+ sh templates/seat-packs-v0/bin/provision-seat claude-code /tmp/dev-claude-code-seat
66
+ sh templates/seat-packs-v0/bin/provision-seat gemini /tmp/dev-gemini-seat
67
+ sh templates/seat-packs-v0/bin/provision-seat openrouter /tmp/dev-openrouter-seat
68
+ sh templates/seat-packs-v0/bin/provision-seat ollama /tmp/dev-ollama-seat
69
+ ```
@@ -0,0 +1,33 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
5
+
6
+ if [ "$#" -ne 1 ]; then
7
+ echo "usage: sh bin/stamp-vanilla <target-dir>" >&2
8
+ exit 2
9
+ fi
10
+
11
+ TARGET=$1
12
+
13
+ if [ -e "$TARGET" ]; then
14
+ echo "target already exists: $TARGET" >&2
15
+ exit 2
16
+ fi
17
+
18
+ mkdir -p "$TARGET/source"
19
+ cp -R "$ROOT/source/." "$TARGET/source/"
20
+ cp "$ROOT/README.md" "$TARGET/README.md"
21
+ cp "$ROOT/validate-vanilla.sh" "$TARGET/validate-vanilla.sh"
22
+
23
+ for repo in etiquette-kernel control-seat; do
24
+ repo_dir="$TARGET/source/$repo"
25
+ git -C "$repo_dir" init -q
26
+ git -C "$repo_dir" add .
27
+ git -C "$repo_dir" \
28
+ -c user.name="Etiquette Vanilla" \
29
+ -c user.email="etiquette-vanilla@example.invalid" \
30
+ commit -q -m "Initial $repo scaffold"
31
+ done
32
+
33
+ echo "stamped etiquette vanilla rollout at $TARGET"
@@ -0,0 +1,22 @@
1
+ # Control Seat Handoff
2
+
3
+ You are the initial Etiquette control seat.
4
+
5
+ ## Startup
6
+
7
+ 1. Run `sh bin/seat-doctor`.
8
+ 2. Read `docs/work/ledger/LEDGER.md`.
9
+ 3. Pick or create one small task.
10
+ 4. Return a receipt using the kernel receipt shape.
11
+
12
+ ## Authority
13
+
14
+ The control seat may initialize local workflow, report readiness, and decide whether more seats are needed. It does not assume any runtime-specific worker pool.
15
+
16
+ ## Guardrails
17
+
18
+ - Do not clone product repos until assigned.
19
+ - Do not assume chat memory is canonical.
20
+ - Do not provision extra seats unless the task needs them.
21
+ - Keep receipts compact and durable.
22
+
@@ -0,0 +1,16 @@
1
+ # Control Seat
2
+
3
+ Generic first seat for Etiquette.
4
+
5
+ The control seat is runtime-neutral. Use it before provisioning Claude Code, Codex, Gemini, or any other runtime-specific seat pack.
6
+
7
+ For local-only proof, append events with `bin/local-event` and compact them with `bin/compact-local-events`.
8
+
9
+ Inside a live runtime session, run `bin/session-init` after `bin/seat-init`
10
+ to prepare the local ledger, receipts, tasks, outbox, and session state.
11
+
12
+ Access assurance is explicit and optional. The default profile is
13
+ `docs/work/access/workspace-secure-profile.v0.json` with `mode: open`. Run
14
+ `bin/access-assurance-check` before routing restricted seats, and switch to
15
+ `screening-warn`, `screening-enforced`, or `enterprise-reactor` only when the
16
+ workspace actually needs provider-backed screening.
@@ -0,0 +1,121 @@
1
+ #!/bin/sh
2
+ # curl-wrapped HTTP helpers for the seat-bridge commands. Surfaces 4xx
3
+ # error bodies with helpful remediation; retries on 5xx (3 attempts,
4
+ # exponential backoff).
5
+ #
6
+ # Stdout: response body on 2xx.
7
+ # Stderr: error details on 4xx/5xx.
8
+ # Exit: 0 on 2xx; non-zero matching the rough HTTP class otherwise
9
+ # (10 for 4xx, 20 for 5xx-after-retries, 30 for network failure).
10
+
11
+ # shellcheck shell=sh
12
+
13
+ ETQ_HTTP_CURL=${ETIQUETTE_CURL:-curl}
14
+ ETQ_HTTP_RETRIES=${ETIQUETTE_HTTP_RETRIES:-3}
15
+ ETQ_HTTP_TIMEOUT=${ETIQUETTE_HTTP_TIMEOUT:-15}
16
+
17
+ # POST helper. Args: URL, JSON body. Optional 3rd arg: extra header line
18
+ # (e.g. "authorization: Bearer ...").
19
+ etq_http_post() {
20
+ url=$1
21
+ body=$2
22
+ hdr=${3:-}
23
+
24
+ attempt=1
25
+ while [ "$attempt" -le "$ETQ_HTTP_RETRIES" ]; do
26
+ tmpfile=$(mktemp -t etq-http.XXXXXX)
27
+ set +e
28
+ if [ -n "$hdr" ]; then
29
+ status=$("$ETQ_HTTP_CURL" -sS -o "$tmpfile" -w '%{http_code}' \
30
+ --max-time "$ETQ_HTTP_TIMEOUT" \
31
+ -X POST -H 'content-type: application/json' -H "$hdr" \
32
+ --data "$body" "$url" 2>/dev/null)
33
+ else
34
+ status=$("$ETQ_HTTP_CURL" -sS -o "$tmpfile" -w '%{http_code}' \
35
+ --max-time "$ETQ_HTTP_TIMEOUT" \
36
+ -X POST -H 'content-type: application/json' \
37
+ --data "$body" "$url" 2>/dev/null)
38
+ fi
39
+ curl_rc=$?
40
+ set -e
41
+
42
+ if [ "$curl_rc" -ne 0 ]; then
43
+ printf 'http: curl failed for %s (rc=%s, attempt %s/%s)\n' \
44
+ "$url" "$curl_rc" "$attempt" "$ETQ_HTTP_RETRIES" >&2
45
+ rm -f "$tmpfile"
46
+ attempt=$((attempt + 1))
47
+ [ "$attempt" -le "$ETQ_HTTP_RETRIES" ] && sleep $((attempt * 2))
48
+ continue
49
+ fi
50
+
51
+ case "$status" in
52
+ 2*)
53
+ cat "$tmpfile"
54
+ rm -f "$tmpfile"
55
+ return 0
56
+ ;;
57
+ 4*)
58
+ printf 'http: %s returned %s\n' "$url" "$status" >&2
59
+ printf 'http: response body:\n' >&2
60
+ cat "$tmpfile" >&2
61
+ printf '\n' >&2
62
+ etq_http_4xx_hint "$status" "$tmpfile" >&2
63
+ rm -f "$tmpfile"
64
+ return 10
65
+ ;;
66
+ 5*)
67
+ if [ "$attempt" -lt "$ETQ_HTTP_RETRIES" ]; then
68
+ printf 'http: %s returned %s (attempt %s/%s; retrying)\n' \
69
+ "$url" "$status" "$attempt" "$ETQ_HTTP_RETRIES" >&2
70
+ rm -f "$tmpfile"
71
+ sleep $((attempt * 2))
72
+ attempt=$((attempt + 1))
73
+ continue
74
+ fi
75
+ printf 'http: %s returned %s after %s retries\n' \
76
+ "$url" "$status" "$ETQ_HTTP_RETRIES" >&2
77
+ cat "$tmpfile" >&2
78
+ rm -f "$tmpfile"
79
+ return 20
80
+ ;;
81
+ *)
82
+ printf 'http: %s returned unexpected status %s\n' "$url" "$status" >&2
83
+ cat "$tmpfile" >&2
84
+ rm -f "$tmpfile"
85
+ return 30
86
+ ;;
87
+ esac
88
+ done
89
+
90
+ return 30
91
+ }
92
+
93
+ # Print a friendly remediation hint for 4xx responses.
94
+ etq_http_4xx_hint() {
95
+ status=$1
96
+ body_file=$2
97
+ case "$status" in
98
+ 400)
99
+ printf '\nhint: request was rejected. Most common causes:\n' >&2
100
+ printf ' - sanitizer rejected an input (path/hostname/IP/length)\n' >&2
101
+ printf ' - missing required field in body\n' >&2
102
+ printf ' - event_type not in allowlist (telemetry only)\n' >&2
103
+ ;;
104
+ 401)
105
+ printf '\nhint: not authenticated. This endpoint requires pass_id OR JWT.\n' >&2
106
+ printf ' - confirm seat-consume-pass was run and workspace.json exists\n' >&2
107
+ printf ' - or pass --workspace-jwt <token> if running admin-side\n' >&2
108
+ ;;
109
+ 403)
110
+ printf '\nhint: forbidden. Your role lacks the required action permission.\n' >&2
111
+ ;;
112
+ 404)
113
+ printf '\nhint: workspace_id or pass_id not found.\n' >&2
114
+ ;;
115
+ 410)
116
+ printf '\nhint: pass is revoked, expired, or single-use already consumed.\n' >&2
117
+ printf ' - issue a new pass via the dashboard and re-run seat-consume-pass\n' >&2
118
+ ;;
119
+ *) ;;
120
+ esac
121
+ }
@@ -0,0 +1,191 @@
1
+ #!/bin/sh
2
+ # Client-side input sanitization. Mirrors api/sanitize.js rejection rules
3
+ # from the hosted-pass-plane V0 prototype so we surface 400 reasons before
4
+ # a round trip.
5
+ #
6
+ # Per hosted-plane boundary rules:
7
+ # - opaque IDs (seat_id, environment_node_id, consumer_seat_id) reject
8
+ # hostname-shape suffixes, IPv4/IPv6, user-home / drive-letter / shell-
9
+ # home / private-temp / absolute-temp path patterns, unsafe characters,
10
+ # whitespace, and length > 64.
11
+ # - repo-relative paths reject absolute / drive-letter / known leakage
12
+ # patterns and length > 256.
13
+ # - artifact_ref allows repo:branch but no absolute paths; length > 128.
14
+ #
15
+ # Each function prints the cleaned value on stdout and returns 0 on success;
16
+ # on rejection it prints a one-line reason to stderr and returns 1.
17
+
18
+ # shellcheck shell=sh
19
+
20
+ # Path-prefix fragments built at runtime via string concatenation so the
21
+ # literals do not appear in source (release-scan would flag them). Same
22
+ # trick scripts/release-scan.sh uses for its own pattern construction.
23
+ _etq_path_p1='/U'"sers/"
24
+ _etq_path_p2='/h'"ome/"
25
+ _etq_path_p3='/pr'"ivate/"
26
+ _etq_path_p4='/t'"mp/"
27
+ _etq_path_p5='$H'"OME"
28
+
29
+ # Reject path-shape patterns. Returns 0 if string contains a known
30
+ # leakage pattern; 1 if clean.
31
+ _etq_has_path_shape() {
32
+ case "$1" in
33
+ *"$_etq_path_p1"*) return 0 ;;
34
+ *"$_etq_path_p2"*) return 0 ;;
35
+ *"$_etq_path_p5"*) return 0 ;;
36
+ *"$_etq_path_p3"*) return 0 ;;
37
+ *"$_etq_path_p4"*) return 0 ;;
38
+ [A-Za-z]:\\*) return 0 ;;
39
+ [A-Za-z]:/*) return 0 ;;
40
+ esac
41
+ return 1
42
+ }
43
+
44
+ # Reject hostname-shape suffixes + IPv4 + (loose) IPv6.
45
+ _etq_has_hostname_shape() {
46
+ case "$1" in
47
+ *.local|*.LOCAL|*.local/*) return 0 ;;
48
+ *.lan|*.LAN|*.lan/*) return 0 ;;
49
+ *.internal|*.INTERNAL|*.internal/*) return 0 ;;
50
+ *.intranet|*.INTRANET|*.intranet/*) return 0 ;;
51
+ *.localdomain|*.LOCALDOMAIN|*.localdomain/*) return 0 ;;
52
+ esac
53
+ # IPv4 quad
54
+ if printf '%s' "$1" | grep -qE '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'; then
55
+ return 0
56
+ fi
57
+ # IPv6 loose (>=2 hex segments separated by colons)
58
+ if printf '%s' "$1" | grep -qiE '\b[0-9a-f]{1,4}:[0-9a-f]{1,4}:'; then
59
+ return 0
60
+ fi
61
+ return 1
62
+ }
63
+
64
+ # Sanitize an opaque ID (seat_id / environment_node_id / consumer_seat_id).
65
+ # Echoes cleaned value on success.
66
+ etq_sanitize_opaque_id() {
67
+ v=$(printf '%s' "$1" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
68
+ if [ -z "$v" ]; then
69
+ echo "sanitize: opaque-id empty" >&2
70
+ return 1
71
+ fi
72
+ if [ "${#v}" -gt 64 ]; then
73
+ echo "sanitize: opaque-id length > 64" >&2
74
+ return 1
75
+ fi
76
+ if _etq_has_path_shape "$v"; then
77
+ echo "sanitize: opaque-id contains path-shape" >&2
78
+ return 1
79
+ fi
80
+ if _etq_has_hostname_shape "$v"; then
81
+ echo "sanitize: opaque-id contains hostname/IP-shape" >&2
82
+ return 1
83
+ fi
84
+ # Disallow whitespace + control chars
85
+ if printf '%s' "$v" | grep -qE '[[:space:][:cntrl:]]'; then
86
+ echo "sanitize: opaque-id contains whitespace/control chars" >&2
87
+ return 1
88
+ fi
89
+ # Keep IDs JSON/URL/log friendly for shell callers that may need to print
90
+ # or embed the value before jq is available.
91
+ if printf '%s' "$v" | grep -qE '[^A-Za-z0-9._:-]'; then
92
+ echo "sanitize: opaque-id contains unsafe characters" >&2
93
+ return 1
94
+ fi
95
+ printf '%s' "$v"
96
+ }
97
+
98
+ # Sanitize a repo-relative path (receipt_ref). NOT absolute.
99
+ etq_sanitize_repo_relative() {
100
+ v=$(printf '%s' "$1" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
101
+ if [ -z "$v" ]; then
102
+ echo "sanitize: repo-relative empty" >&2
103
+ return 1
104
+ fi
105
+ if [ "${#v}" -gt 256 ]; then
106
+ echo "sanitize: repo-relative length > 256" >&2
107
+ return 1
108
+ fi
109
+ case "$v" in
110
+ /*) echo "sanitize: repo-relative must not be absolute (starts with /)" >&2; return 1 ;;
111
+ \\*) echo "sanitize: repo-relative must not be absolute (starts with \\)" >&2; return 1 ;;
112
+ [A-Za-z]:[\\/]*) echo "sanitize: repo-relative must not be drive-letter absolute" >&2; return 1 ;;
113
+ esac
114
+ if _etq_has_path_shape "$v"; then
115
+ echo "sanitize: repo-relative contains leakage path-shape" >&2
116
+ return 1
117
+ fi
118
+ printf '%s' "$v"
119
+ }
120
+
121
+ # Sanitize telemetry meta JSON. Mirrors telemetry.v0:
122
+ # - object only
123
+ # - keys non-empty and <= 32 chars
124
+ # - string values <= 64 chars and not path/hostname/IP-shaped
125
+ # - numeric and boolean values kept
126
+ # - arrays/objects/null stripped
127
+ #
128
+ # Prints compact JSON object on success.
129
+ etq_sanitize_meta_json() {
130
+ if ! command -v jq >/dev/null 2>&1; then
131
+ echo "sanitize: jq required for telemetry meta" >&2
132
+ return 1
133
+ fi
134
+
135
+ printf '%s' "$1" | jq -c \
136
+ --arg p1 "$_etq_path_p1" \
137
+ --arg p2 "$_etq_path_p2" \
138
+ --arg p3 "$_etq_path_p3" \
139
+ --arg p4 "$_etq_path_p4" \
140
+ --arg p5 "$_etq_path_p5" \
141
+ '
142
+ def bad_path:
143
+ contains($p1) or contains($p2) or contains($p3) or
144
+ contains($p4) or contains($p5) or test("^[A-Za-z]:[\\\\/]");
145
+ def bad_host:
146
+ test("\\.(local|lan|internal|intranet|localdomain)$"; "i") or
147
+ test("^[0-9]{1,3}(\\.[0-9]{1,3}){3}") or
148
+ test("[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}:");
149
+
150
+ if type != "object" then
151
+ error("telemetry meta must be an object")
152
+ else
153
+ with_entries(
154
+ select(
155
+ (.key | length > 0 and length <= 32) and
156
+ (
157
+ ((.value | type) == "string" and
158
+ (.value | length <= 64) and
159
+ ((.value | bad_path) | not) and
160
+ ((.value | bad_host) | not))
161
+ or ((.value | type) == "number")
162
+ or ((.value | type) == "boolean")
163
+ )
164
+ )
165
+ )
166
+ end
167
+ '
168
+ }
169
+
170
+ # Sanitize an artifact_ref. Allows owner/repo:branch but no absolute.
171
+ etq_sanitize_artifact_ref() {
172
+ v=$(printf '%s' "$1" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
173
+ if [ -z "$v" ]; then
174
+ echo "sanitize: artifact-ref empty" >&2
175
+ return 1
176
+ fi
177
+ if [ "${#v}" -gt 128 ]; then
178
+ echo "sanitize: artifact-ref length > 128" >&2
179
+ return 1
180
+ fi
181
+ case "$v" in
182
+ /*) echo "sanitize: artifact-ref must not be absolute" >&2; return 1 ;;
183
+ \\*) echo "sanitize: artifact-ref must not be absolute" >&2; return 1 ;;
184
+ [A-Za-z]:[\\/]*) echo "sanitize: artifact-ref must not be drive-letter absolute" >&2; return 1 ;;
185
+ esac
186
+ if _etq_has_path_shape "$v"; then
187
+ echo "sanitize: artifact-ref contains leakage path-shape" >&2
188
+ return 1
189
+ fi
190
+ printf '%s' "$v"
191
+ }
@@ -0,0 +1,67 @@
1
+ #!/bin/sh
2
+ # Local seat state read/write helpers.
3
+ #
4
+ # State files are per-seat at V0. A future outbox lane may split these
5
+ # per workspace if the rotation model needs it:
6
+ #
7
+ # $STATE_DIR/seat.json { seat_id, execution_seat, created_at }
8
+ # $STATE_DIR/workspace.json cached pass envelope after seat-consume-pass
9
+ #
10
+ # STATE_DIR defaults to $ROOT/.etiquette/state. File mode 600 on create.
11
+ #
12
+ # All read functions echo the value on stdout; missing files / keys
13
+ # return non-zero with a stderr message.
14
+
15
+ # shellcheck shell=sh
16
+
17
+ etq_state_dir() {
18
+ # $ROOT must be set by the caller (matches local-event / seat-doctor pattern)
19
+ printf '%s' "${ETIQUETTE_STATE_DIR:-$ROOT/.etiquette/state}"
20
+ }
21
+
22
+ etq_state_init_dir() {
23
+ dir=$(etq_state_dir)
24
+ mkdir -p "$dir"
25
+ # Best-effort restrictive perms; not all filesystems honour them.
26
+ chmod 700 "$dir" 2>/dev/null || true
27
+ }
28
+
29
+ # Read a top-level key from a JSON file. Args: file-path, key.
30
+ # Exits non-zero if file missing or key not present.
31
+ etq_state_read_key() {
32
+ file=$1
33
+ key=$2
34
+ if [ ! -f "$file" ]; then
35
+ printf 'state: file missing: %s\n' "$file" >&2
36
+ return 1
37
+ fi
38
+ if ! command -v jq >/dev/null 2>&1; then
39
+ printf 'state: jq is required but not on PATH\n' >&2
40
+ return 2
41
+ fi
42
+ out=$(jq -r --arg k "$key" '.[$k] // empty' "$file")
43
+ if [ -z "$out" ]; then
44
+ printf 'state: key not present: %s in %s\n' "$key" "$file" >&2
45
+ return 3
46
+ fi
47
+ printf '%s' "$out"
48
+ }
49
+
50
+ # Write a complete JSON object to a state file. Args: file-path, json-body.
51
+ # Creates parent dir + sets mode 600.
52
+ etq_state_write_file() {
53
+ file=$1
54
+ body=$2
55
+ dir=$(dirname -- "$file")
56
+ mkdir -p "$dir"
57
+ chmod 700 "$dir" 2>/dev/null || true
58
+ # Use a temp file + mv for atomicity.
59
+ tmp="$file.tmp.$$"
60
+ printf '%s' "$body" > "$tmp"
61
+ chmod 600 "$tmp" 2>/dev/null || true
62
+ mv "$tmp" "$file"
63
+ }
64
+
65
+ # Convenience: print path to seat.json / workspace.json.
66
+ etq_state_seat_path() { printf '%s/seat.json' "$(etq_state_dir)"; }
67
+ etq_state_workspace_path() { printf '%s/workspace.json' "$(etq_state_dir)"; }
@@ -0,0 +1,55 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
5
+ PROFILE="$ROOT/docs/work/access/workspace-secure-profile.v0.json"
6
+ POLICY="$ROOT/docs/work/access/seat-access-policy.v0.json"
7
+ CLAIMS="$ROOT/docs/work/access/access-assurance-claims.v0.json"
8
+
9
+ json_string_field() {
10
+ sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" "$1" | head -n 1
11
+ }
12
+
13
+ json_bool_field() {
14
+ sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\\(true\\|false\\).*/\\1/p" "$1" | head -n 1
15
+ }
16
+
17
+ if [ ! -f "$PROFILE" ]; then
18
+ echo "access-assurance: fail missing docs/work/access/workspace-secure-profile.v0.json" >&2
19
+ exit 1
20
+ fi
21
+
22
+ mode=$(json_string_field "$PROFILE" mode)
23
+ case "$mode" in
24
+ open|screening-warn|screening-enforced|enterprise-reactor) ;;
25
+ *)
26
+ echo "access-assurance: fail invalid mode=${mode:-missing}" >&2
27
+ exit 1
28
+ ;;
29
+ esac
30
+
31
+ if [ "$mode" = "open" ]; then
32
+ echo "access-assurance: pass mode=open policy=not-required"
33
+ exit 0
34
+ fi
35
+
36
+ if [ ! -f "$POLICY" ] || [ ! -f "$CLAIMS" ]; then
37
+ message="access-assurance: missing policy or claims for mode=$mode"
38
+ if [ "$mode" = "screening-warn" ]; then
39
+ echo "$message"
40
+ exit 0
41
+ fi
42
+ echo "$message" >&2
43
+ exit 1
44
+ fi
45
+
46
+ if [ "$mode" = "enterprise-reactor" ]; then
47
+ tighten_only=$(json_bool_field "$PROFILE" tighten_only)
48
+ reactor_can_grant=$(json_bool_field "$PROFILE" reactor_can_grant_or_loosen_access)
49
+ if [ "$tighten_only" != "true" ] || [ "$reactor_can_grant" != "false" ]; then
50
+ echo "access-assurance: fail enterprise-reactor requires tighten_only=true and reactor_can_grant_or_loosen_access=false" >&2
51
+ exit 1
52
+ fi
53
+ fi
54
+
55
+ echo "access-assurance: pass mode=$mode"
@@ -0,0 +1,71 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
5
+
6
+ if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
7
+ echo "usage: sh bin/compact-local-events <task-id> [success|partial|blocked|failed]" >&2
8
+ exit 2
9
+ fi
10
+
11
+ task_id=$1
12
+ status=${2:-success}
13
+ operator_principal=${ETIQUETTE_OPERATOR_PRINCIPAL:-local-operator}
14
+ execution_seat=${ETIQUETTE_EXECUTION_SEAT:-control-seat}
15
+ event_log=${ETIQUETTE_EVENT_LOG:-"$ROOT/.etiquette/events/local.ndjson"}
16
+ receipt_dir=${ETIQUETTE_RECEIPT_DIR:-"$ROOT/docs/work/receipts"}
17
+
18
+ case "$status" in
19
+ success|partial|blocked|failed) ;;
20
+ *)
21
+ echo "invalid receipt status: $status" >&2
22
+ exit 2
23
+ ;;
24
+ esac
25
+
26
+ if [ ! -f "$event_log" ]; then
27
+ echo "missing local event log: $event_log" >&2
28
+ exit 1
29
+ fi
30
+
31
+ task_pattern='"task_id":"'"$task_id"'"'
32
+ matches=$(grep -F "$task_pattern" "$event_log" || true)
33
+
34
+ if [ -z "$matches" ]; then
35
+ echo "no events found for task: $task_id" >&2
36
+ exit 1
37
+ fi
38
+
39
+ event_count=$(printf '%s\n' "$matches" | sed '/^$/d' | wc -l | tr -d ' ')
40
+ first_seq=$(printf '%s\n' "$matches" | sed -n '1s/.*"seq":\([0-9][0-9]*\).*/\1/p')
41
+ last_seq=$(printf '%s\n' "$matches" | sed -n '$s/.*"seq":\([0-9][0-9]*\).*/\1/p')
42
+ last_action=$(printf '%s\n' "$matches" | sed -n '$s/.*"action":"\([^"]*\)".*/\1/p')
43
+
44
+ mkdir -p "$receipt_dir"
45
+ receipt="$receipt_dir/$task_id-receipt.yaml"
46
+ tmp="$receipt.tmp.$$"
47
+
48
+ cat > "$tmp" <<EOF
49
+ task_id: $task_id
50
+ operator_principal: $operator_principal
51
+ execution_seat: $execution_seat
52
+ status: $status
53
+ sources_read:
54
+ - .etiquette/events/local.ndjson
55
+ files_changed:
56
+ - docs/work/receipts/$task_id-receipt.yaml
57
+ validation:
58
+ - sh bin/seat-doctor
59
+ risks: []
60
+ next_owner: control-seat
61
+ compaction:
62
+ scope: local-only
63
+ event_log: .etiquette/events/local.ndjson
64
+ event_count: $event_count
65
+ first_seq: $first_seq
66
+ last_seq: $last_seq
67
+ last_action: $last_action
68
+ EOF
69
+
70
+ mv "$tmp" "$receipt"
71
+ printf 'receipt: %s\n' "$receipt"