@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.
- package/AGENTS.md +28 -0
- package/QuickStart.md +94 -0
- package/README.md +77 -9
- package/bin/etiquette +1149 -0
- package/bin/etiquette-core +313 -0
- package/docs/ARCHITECTURE.md +100 -0
- package/docs/CODEX_CLIENT_COMPATIBILITY.md +83 -0
- package/docs/CONCEPTS.md +128 -0
- package/docs/CONCEPT_STATUS.md +67 -0
- package/docs/CORE_PROFILE.md +70 -0
- package/docs/LANE_PROVISIONING.md +157 -0
- package/docs/README.md +29 -0
- package/docs/RELEASE_SURFACE_AUDIT.md +156 -0
- package/docs/SEAT_DISCIPLINE.md +68 -0
- package/docs/SEAT_PROVISIONING.md +100 -0
- package/docs/TEAM_HANDOFF.md +190 -0
- package/docs/WORKTREE_QOL.md +92 -0
- package/docs/contracts/ledger-entry/README.md +83 -0
- package/docs/contracts/ledger-entry/ledger-entry.v0.1.md +116 -0
- package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +90 -0
- package/docs/contracts/ledger-entry/ledger-entry.v0.md +123 -0
- package/docs/contracts/ledger-entry/ledger-query-cli.v0.1.md +64 -0
- package/package.json +42 -11
- package/scripts/install.sh +144 -0
- package/scripts/uninstall.sh +58 -0
- package/templates/etiquette-vanilla-v0/README.md +69 -0
- package/templates/etiquette-vanilla-v0/bin/stamp-vanilla +33 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/.llm-substrate/HANDOFF.md +22 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/README.md +16 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/_lib/http.sh +121 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/_lib/sanitize.sh +191 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/_lib/state.sh +67 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +55 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/compact-local-events +71 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/post-tool-use +64 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/session-end +64 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/session-start +64 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/stop +64 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/hooks/user-prompt-submit +64 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/local-event +53 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/post-receipt +143 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/post-telemetry +112 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-consume-pass +115 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +20 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-init +86 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-post-readiness +171 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/session-init +132 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +39 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +53 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/ledger/LEDGER.md +7 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/playbacks/PLAYBACK_TEMPLATE.md +31 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/receipts/.gitkeep +1 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/tasks/CONTROL-001.yaml +13 -0
- package/templates/etiquette-vanilla-v0/source/etiquette-kernel/README.md +6 -0
- package/templates/etiquette-vanilla-v0/source/etiquette-kernel/bin/kernel-doctor +11 -0
- package/templates/etiquette-vanilla-v0/source/etiquette-kernel/contracts/playback.v0.md +37 -0
- package/templates/etiquette-vanilla-v0/source/etiquette-kernel/contracts/receipt.v0.md +25 -0
- package/templates/etiquette-vanilla-v0/source/etiquette-kernel/contracts/seat-readiness.v0.md +22 -0
- package/templates/etiquette-vanilla-v0/source/etiquette-kernel/contracts/task.v0.md +18 -0
- package/templates/etiquette-vanilla-v0/validate-vanilla.sh +43 -0
- package/templates/github-actions/README.md +30 -0
- package/templates/github-actions/w1-webhook-wake-canary.yml +111 -0
- package/templates/hosted-receiver/README.md +41 -0
- package/templates/hosted-receiver/w1-github-webhook-receiver.mjs +129 -0
- package/templates/seat-packs-v0/README.md +72 -0
- package/templates/seat-packs-v0/bin/provision-seat +70 -0
- package/templates/seat-packs-v0/source/claude-code-seat/.llm-substrate/HANDOFF.md +24 -0
- package/templates/seat-packs-v0/source/claude-code-seat/README.md +19 -0
- package/templates/seat-packs-v0/source/claude-code-seat/bin/seat-doctor +8 -0
- package/templates/seat-packs-v0/source/codex-seat/.llm-substrate/HANDOFF.md +25 -0
- package/templates/seat-packs-v0/source/codex-seat/README.md +22 -0
- package/templates/seat-packs-v0/source/codex-seat/bin/seat-doctor +8 -0
- package/templates/seat-packs-v0/source/gemini-seat/.llm-substrate/HANDOFF.md +23 -0
- package/templates/seat-packs-v0/source/gemini-seat/README.md +19 -0
- package/templates/seat-packs-v0/source/gemini-seat/bin/seat-doctor +8 -0
- package/templates/seat-packs-v0/source/ollama-seat/.llm-substrate/HANDOFF.md +24 -0
- package/templates/seat-packs-v0/source/ollama-seat/README.md +19 -0
- package/templates/seat-packs-v0/source/ollama-seat/bin/seat-doctor +8 -0
- package/templates/seat-packs-v0/source/openrouter-seat/.llm-substrate/HANDOFF.md +24 -0
- package/templates/seat-packs-v0/source/openrouter-seat/README.md +19 -0
- package/templates/seat-packs-v0/source/openrouter-seat/bin/seat-doctor +8 -0
- package/templates/seat-packs-v0/validate-seat-packs.sh +27 -0
- 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"
|