agent-temporary 0.7.0

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/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # agent-temporary
2
+
3
+ agent-temporary 0.7.0 — small utility for explicitly bounded temporary root access on Linux or macOS.
4
+
5
+ ## Contract
6
+
7
+ ```text
8
+ sudo agent-temporary install --user zero
9
+ sudo agent-temporary on
10
+ sudo agent-temporary on --ttl 30m
11
+ sudo agent-temporary on --ttl 2h --persist-reboot
12
+ sudo agent-temporary off
13
+ agent-temporary status
14
+ agent-temporary version
15
+ sudo ./uninstall.sh
16
+ ```
17
+
18
+ The npm distribution installs only an unprivileged setup command:
19
+
20
+ ```sh
21
+ npm install -g agent-temporary
22
+ agent-temporary-setup install
23
+ agent-temporary status
24
+ agent-temporary-setup update
25
+ npm uninstall -g agent-temporary
26
+ agent-temporary-setup uninstall-system
27
+ ```
28
+
29
+ `npm install` and `npm uninstall` affect only the npm package. The explicit setup
30
+ commands invoke the existing system installer or uninstaller and request administrator
31
+ privileges; they do not activate temporary access. To update the setup package, use
32
+ `npm install -g agent-temporary@latest` and then run `agent-temporary-setup update`.
33
+
34
+ Activation defaults to a 5-minute TTL. The allowed range is 5 minutes through 8 hours;
35
+ accepted forms are integer minutes or hours such as `30m`, `1h`, and `4h`.
36
+
37
+ On macOS, `--persist-reboot` explicitly preserves the same grant across reboot
38
+ until its original expiry. On Linux, the existing behavior remains unchanged.
39
+ Without persistence, boot revocation removes
40
+ temporary privilege.
41
+
42
+ While active, the configured user has unrestricted `NOPASSWD: ALL` sudo access.
43
+ The privilege is bounded by a local expiry supervisor and is revoked on boot.
44
+ It does not use SSH keys or `authorized_keys`, and does not require Netbot or a
45
+ remote controller to revoke access.
46
+
47
+ `status` reports state from local state/rule inspection and an exact harmless
48
+ `sudo -n /usr/bin/true` execution probe; sudo policy listing alone is not treated
49
+ as effective authority. `status --json` is accepted for machine-readable integration
50
+ (the key/value fields remain stable). State is stored root-owned under
51
+ `/var/lib/agent-temporary/`.
52
+
53
+ ## Platform
54
+
55
+ Supported: Linux with an active systemd or OpenRC runtime and sudo/visudo, or
56
+ macOS with launchd and sudo/visudo. OpenRC is detected through its native runtime
57
+ state and service tools, including their standard `/sbin` locations.
58
+
59
+ Unsupported: Linux with another/unknown init system and unsupported platforms;
60
+ they fail closed and are not given a synthetic systemd setup.
61
+
62
+ ## Release
63
+
64
+ Build a release artifact with:
65
+
66
+ ```sh
67
+ ./release.sh
68
+ ```
69
+
70
+ The resulting directory contains the executable, systemd/OpenRC/launchd service definitions, `VERSION`,
71
+ `SHA256SUMS`, and a deterministic `install.sh`. It also creates
72
+ `dist/agent-temporary-X.Y.Z.zip` and its SHA-256 sidecar. The archive contains no Git metadata,
73
+ runtime state, logs, or private material; after extraction, run
74
+ `sudo ./install.sh --user zero` and the extracted source directory may be removed.
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const crypto = require('crypto');
7
+ const cp = require('child_process');
8
+
9
+ const packageRoot = path.resolve(__dirname, '..');
10
+ const pkg = require(path.join(packageRoot, 'package.json'));
11
+ const payload = path.join(packageRoot, 'payload');
12
+ const manifestPath = path.join(packageRoot, 'manifest.json');
13
+
14
+ function fail(message) {
15
+ process.stderr.write(`agent-temporary-setup: ${message}\n`);
16
+ process.exitCode = 1;
17
+ }
18
+
19
+ function usage() {
20
+ process.stdout.write('Usage:\n' +
21
+ ' agent-temporary-setup install\n' +
22
+ ' agent-temporary-setup update\n' +
23
+ ' agent-temporary-setup status\n' +
24
+ ' agent-temporary-setup uninstall-system\n');
25
+ }
26
+
27
+ function versionParts(value) {
28
+ const match = /^([0-9]+)\.([0-9]+)\.([0-9]+)$/.exec(value || '');
29
+ return match ? match.slice(1).map(Number) : null;
30
+ }
31
+
32
+ function compareVersions(left, right) {
33
+ const a = versionParts(left);
34
+ const b = versionParts(right);
35
+ if (!a || !b) return null;
36
+ for (let i = 0; i < 3; i += 1) {
37
+ if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
38
+ }
39
+ return 0;
40
+ }
41
+
42
+ function verifyPayload() {
43
+ let manifest;
44
+ try {
45
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
46
+ } catch (error) {
47
+ throw new Error(`cannot read payload manifest: ${error.message}`);
48
+ }
49
+ if (manifest.version !== pkg.version || manifest.payload_version !== pkg.version) {
50
+ throw new Error('package version and embedded payload version do not match');
51
+ }
52
+ const version = fs.readFileSync(path.join(payload, 'VERSION'), 'utf8').trim();
53
+ if (version !== pkg.version) throw new Error('embedded VERSION does not match package version');
54
+ const checksumFile = path.join(packageRoot, manifest.payload_checksums || 'payload/SHA256SUMS');
55
+ const lines = fs.readFileSync(checksumFile, 'utf8').trim().split(/\n/).filter(Boolean);
56
+ if (!lines.length) throw new Error('embedded payload checksums are empty');
57
+ for (const line of lines) {
58
+ const match = /^([0-9a-f]{64})\s+(.+)$/.exec(line);
59
+ if (!match) throw new Error(`malformed payload checksum line: ${line}`);
60
+ const relative = match[2];
61
+ const resolved = path.resolve(payload, relative);
62
+ if (resolved !== payload && !resolved.startsWith(`${payload}${path.sep}`)) {
63
+ throw new Error(`payload checksum escapes payload: ${relative}`);
64
+ }
65
+ const actual = crypto.createHash('sha256').update(fs.readFileSync(resolved)).digest('hex');
66
+ if (actual !== match[1]) throw new Error(`payload integrity check failed: ${relative}`);
67
+ }
68
+ for (const required of ['install.sh', 'uninstall.sh', 'agent-temporary', 'VERSION']) {
69
+ if (!fs.existsSync(path.join(payload, required))) throw new Error(`payload is missing ${required}`);
70
+ }
71
+ }
72
+
73
+ function installedSystem() {
74
+ const result = cp.spawnSync('agent-temporary', ['version'], { encoding: 'utf8' });
75
+ if (result.error || result.status !== 0) return { present: false, version: null };
76
+ const output = `${result.stdout || ''}${result.stderr || ''}`;
77
+ const match = /agent-temporary\s+([^\s]+)/.exec(output);
78
+ return { present: true, version: match ? match[1] : null };
79
+ }
80
+
81
+ function runSudo(args) {
82
+ return cp.spawnSync('sudo', args, { stdio: 'inherit' });
83
+ }
84
+
85
+ function verifyInactive() {
86
+ const result = cp.spawnSync('sudo', ['agent-temporary', 'status'], { encoding: 'utf8' });
87
+ if (result.error || result.status !== 0) throw new Error('could not verify final system status');
88
+ const output = `${result.stdout || ''}${result.stderr || ''}`;
89
+ process.stdout.write(output);
90
+ if (!/^state=inactive$/m.test(output) || !/^effective_authority=false$/m.test(output)) {
91
+ throw new Error('system installation is not inactive after setup');
92
+ }
93
+ }
94
+
95
+ function doStatus() {
96
+ const system = installedSystem();
97
+ process.stdout.write(`npm/package version=${pkg.version}\n`);
98
+ process.stdout.write(`embedded payload version=${pkg.version}\n`);
99
+ process.stdout.write(`system components=${system.present ? 'present' : 'absent/unknown'}\n`);
100
+ process.stdout.write(`installed system version=${system.version || 'unknown'}\n`);
101
+ if (!system.present) process.stdout.write('update available=no (system installation not detected)\n');
102
+ else {
103
+ const comparison = compareVersions(system.version, pkg.version);
104
+ if (comparison === null) process.stdout.write('update available=unknown\n');
105
+ else if (comparison < 0) process.stdout.write('update available=yes\n');
106
+ else process.stdout.write('update available=no\n');
107
+ }
108
+ }
109
+
110
+ function installOrUpdate(command) {
111
+ verifyPayload();
112
+ const system = installedSystem();
113
+ const comparison = system.version ? compareVersions(system.version, pkg.version) : null;
114
+ if (system.present && comparison === null) throw new Error('installed system version is unknown; refusing ambiguous installation');
115
+ if (comparison > 0) throw new Error(`installed system version ${system.version} is newer than package version ${pkg.version}; downgrade is not supported`);
116
+ if (command === 'update' && !system.present) throw new Error('no installed system detected; use install');
117
+ if (comparison === 0) {
118
+ process.stdout.write(`agent-temporary ${pkg.version} is already current; no system changes made.\n`);
119
+ return;
120
+ }
121
+ process.stdout.write(`Installing agent-temporary ${pkg.version} system components.\nAdministrator privileges are required.\n`);
122
+ const result = runSudo(['sh', path.join(payload, 'install.sh')]);
123
+ if (result.error || result.status !== 0) throw new Error('system installer failed');
124
+ const installed = installedSystem();
125
+ if (!installed.present || installed.version !== pkg.version) throw new Error('installed system version verification failed');
126
+ verifyInactive();
127
+ process.stdout.write(`agent-temporary ${pkg.version} system installation is present and inactive.\n`);
128
+ }
129
+
130
+ function uninstallSystem() {
131
+ verifyPayload();
132
+ process.stdout.write('Removing agent-temporary system components. Administrator privileges are required.\n');
133
+ const result = runSudo(['sh', path.join(payload, 'uninstall.sh')]);
134
+ if (result.error || result.status !== 0) throw new Error('system uninstaller failed');
135
+ const system = installedSystem();
136
+ if (system.present) throw new Error('system command remains after uninstall');
137
+ process.stdout.write('agent-temporary system installation removed.\n');
138
+ }
139
+
140
+ function main() {
141
+ const command = process.argv[2];
142
+ if (!command) return usage();
143
+ if (command === '--help' || command === '-h') return usage();
144
+ if (command === '--version') return process.stdout.write(`agent-temporary-setup ${pkg.version}\n`);
145
+ if (!['status', 'install', 'update', 'uninstall-system'].includes(command)) {
146
+ usage();
147
+ process.exitCode = 2;
148
+ return;
149
+ }
150
+ try {
151
+ if (command === 'status') doStatus();
152
+ else if (command === 'uninstall-system') uninstallSystem();
153
+ else installOrUpdate(command);
154
+ } catch (error) {
155
+ fail(error.message);
156
+ }
157
+ }
158
+
159
+ main();
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const pkg = require('../package.json');
5
+ process.stdout.write(`agent-temporary ${pkg.version} installed.\n\n` +
6
+ 'System components are not installed automatically.\n' +
7
+ 'To finish setup:\n' +
8
+ ' agent-temporary-setup install\n\n' +
9
+ 'This step will request administrator privileges.\n');
package/manifest.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": "0.7.0",
3
+ "payload_version": "0.7.0",
4
+ "payload_checksums": "payload/SHA256SUMS"
5
+ }
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "agent-temporary",
3
+ "version": "0.7.0",
4
+ "description": "Unprivileged setup layer for the agent-temporary system release",
5
+ "bin": {
6
+ "agent-temporary-setup": "bin/agent-temporary-setup.js"
7
+ },
8
+ "files": [
9
+ "bin",
10
+ "payload",
11
+ "manifest.json",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "postinstall": "node bin/postinstall.js"
16
+ },
17
+ "license": "MIT"
18
+ }
@@ -0,0 +1,13 @@
1
+ a41b716bf984f50311a67f6841b624741f534d80bfb7ca9d7e68e2b68a192882 agent-temporary
2
+ 661adb81432a930663d85f3f7a36bff5a6bdc82534012688b3ff34502d792612 agent-temporary-expire.service
3
+ c14e46956698ac589515971fa71171034f94ee0276c97645c363b0729c1d2870 agent-temporary-expire.timer
4
+ 425bc4e3601631c6cf612140905a5e8f1167320278c870cb5d4b6723eadda5e6 agent-temporary-boot.service
5
+ fc2cab450fa5743ba05b46ab8a5aca8d8e418ba39517be52848630a05dda9eb0 agent-temporary-boot.openrc
6
+ 15369ed62ff24f98c19d3d331f32b12f1d0712b5e3cbac9c2cddb5ad22c2e00a agent-temporary-expire.openrc
7
+ d26cd02487363aaa2f633f6c83bd61206fed2297004023b0c3e143c20633f416 macos/agent-temporary-macos
8
+ 1dff1a00db27128e2c618877fd2c76986bc27240c1b5b075a9daa3b8b9d58fce macos/agent-temporary-reaper
9
+ 0cf3a987694cc296331cbbeee0ef3af77dd1e21f1136f5438b5a621b6fe75ce5 macos/com.agent-temporary.expire.plist
10
+ e963a4d32c7b47eb68db3e286ca53675b59ddd22370ad330334115e32a877363 macos/com.agent-temporary.boot.plist
11
+ 967d9afb101346667166f2e76e81910bc190488d7d41d50ca0072e9d92f00e32 VERSION
12
+ 2ee46662f8db2fb0d6536b91cef0aad4239255d91f9485de7b34111125a8ccd7 install.sh
13
+ a52fb0bd6dd7c24069096706c075b35fc511468912970bb6eaadb2bcdc83a32d uninstall.sh
@@ -0,0 +1 @@
1
+ 0.7.0
@@ -0,0 +1,327 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ VERSION=0.7.0
5
+ DEFAULT_TTL=5m
6
+ PREFIX=${PREFIX:-/usr/local/bin}
7
+ TARGET=${TARGET:-$PREFIX/agent-temporary}
8
+ RULE=${AGENT_TEMPORARY_RULE:-/etc/sudoers.d/90-temporary-agent}
9
+ CONFIG=${AGENT_TEMPORARY_CONFIG:-/etc/agent-temporary.conf}
10
+ STATE_DIR=${AGENT_TEMPORARY_STATE_DIR:-/var/lib/agent-temporary}
11
+ STATE=${AGENT_TEMPORARY_STATE:-$STATE_DIR/state}
12
+ SYSTEMD_DIR=${AGENT_TEMPORARY_SYSTEMD_DIR:-/etc/systemd/system}
13
+ SYSTEMCTL=${SYSTEMCTL:-systemctl}
14
+ OPENRC_DIR=${AGENT_TEMPORARY_OPENRC_DIR:-/etc/init.d}
15
+ RCSERVICE=${RCSERVICE:-}
16
+ RCUPDATE=${RCUPDATE:-}
17
+ OPENRC_PIDFILE=${AGENT_TEMPORARY_OPENRC_PIDFILE:-/run/agent-temporary-expire.pid}
18
+ VISUDO=${VISUDO:-visudo}
19
+ SUDOERS=${AGENT_TEMPORARY_SUDOERS:-/etc/sudoers}
20
+ EXPIRY_TIMER=agent-temporary-expire.timer
21
+ SOURCE_DIR=${AGENT_TEMPORARY_RELEASE_DIR:-.}
22
+ UNAME=${UNAME:-uname}
23
+
24
+ OS=$($UNAME -s 2>/dev/null || true)
25
+
26
+ usage() { echo "Usage: sudo $0 {install --user USER|on --ttl DURATION|off|status [--json]|version}" >&2; exit 2; }
27
+ need_root() { [ "$(id -u)" -eq 0 ] || { echo "Run as root: sudo $0 $*" >&2; exit 1; }; }
28
+ die() { echo "agent-temporary: $*" >&2; exit 1; }
29
+ version() { echo "agent-temporary $VERSION"; }
30
+
31
+ load_user() {
32
+ [ -r "$CONFIG" ] || die "not installed: $CONFIG is missing"
33
+ . "$CONFIG"
34
+ [ -n "${AGENT_USER:-}" ] || die "invalid config: AGENT_USER is missing"
35
+ id "$AGENT_USER" >/dev/null 2>&1 || die "unknown configured user: $AGENT_USER"
36
+ }
37
+
38
+ validate_platform() {
39
+ [ "$OS" = Linux ] || die "Linux systemd or OpenRC is required"
40
+ command -v "$VISUDO" >/dev/null 2>&1 || die "visudo is required"
41
+ if command -v "$SYSTEMCTL" >/dev/null 2>&1 && { [ -d /run/systemd/system ] || "$SYSTEMCTL" show-environment >/dev/null 2>&1; }; then
42
+ INIT_SYSTEM=systemd
43
+ else
44
+ [ -n "$RCSERVICE" ] || for candidate in /sbin/rc-service /usr/sbin/rc-service; do [ -x "$candidate" ] && { RCSERVICE=$candidate; break; }; done
45
+ [ -n "$RCUPDATE" ] || for candidate in /sbin/rc-update /usr/sbin/rc-update; do [ -x "$candidate" ] && { RCUPDATE=$candidate; break; }; done
46
+ [ -x "${RCSERVICE:-/nonexistent}" ] && [ -x "${RCUPDATE:-/nonexistent}" ] || die "Linux systemd or OpenRC is required"
47
+ [ -e /run/openrc/softlevel ] || die "OpenRC is not active"
48
+ INIT_SYSTEM=openrc
49
+ fi
50
+ }
51
+
52
+ service_unit() { [ "$1" = agent-temporary-expire ] && [ "$INIT_SYSTEM" = systemd ] && echo agent-temporary-expire.timer || echo "$1"; }
53
+ service_start() { validate_platform; unit=$(service_unit "$1"); if [ "$INIT_SYSTEM" = openrc ]; then "$RCSERVICE" "$unit" start; else "$SYSTEMCTL" start "$unit"; fi; }
54
+ service_stop() { validate_platform; unit=$(service_unit "$1"); if [ "$INIT_SYSTEM" = openrc ]; then "$RCSERVICE" "$unit" stop >/dev/null 2>&1 || true; else "$SYSTEMCTL" stop "$unit" >/dev/null 2>&1 || true; fi; }
55
+ service_active() { validate_platform; unit=$(service_unit "$1"); if [ "$INIT_SYSTEM" = openrc ]; then "$RCSERVICE" "$unit" status >/dev/null 2>&1; else "$SYSTEMCTL" is-active --quiet "$unit"; fi; }
56
+
57
+ parse_ttl() {
58
+ ttl=$1
59
+ case "$ttl" in
60
+ [0-9]*m) number=${ttl%m}; seconds=$((number * 60)) ;;
61
+ [0-9]*h) number=${ttl%h}; seconds=$((number * 3600)) ;;
62
+ *) die "TTL must be an integer number of minutes or hours (30m or 1h)" ;;
63
+ esac
64
+ case "$number" in ''|*[!0-9]*) die "malformed TTL: $ttl" ;; esac
65
+ [ "$seconds" -ge 300 ] || die "TTL minimum is 5m"
66
+ [ "$seconds" -le 28800 ] || die "TTL maximum is 8h"
67
+ echo "$seconds"
68
+ }
69
+
70
+ validate_sudoers() { "$VISUDO" -cf "$SUDOERS" >/dev/null; }
71
+
72
+ effective_unrestricted() {
73
+ # sudo -l describes policy, not effective execution. Probe the exact
74
+ # noninteractive mechanism with a harmless command as the configured
75
+ # user. This also catches Defaults/authenticate and group-rule ordering
76
+ # differences across sudo implementations.
77
+ "$VISUDO" -cf "$SUDOERS" >/dev/null 2>&1 || return 1
78
+ true_command=
79
+ for candidate in /usr/bin/true /bin/true; do
80
+ if [ -x "$candidate" ]; then true_command=$candidate; break; fi
81
+ done
82
+ [ -n "$true_command" ] || return 1
83
+ if [ "$(id -u)" -eq 0 ]; then
84
+ su -s /bin/sh "$1" -c "sudo -n $true_command" >/dev/null 2>&1
85
+ elif [ "$(id -un)" = "$1" ]; then
86
+ sudo -n "$true_command" >/dev/null 2>&1
87
+ else
88
+ return 1
89
+ fi
90
+ }
91
+
92
+ write_rule() {
93
+ tmp=$(mktemp "${RULE}.XXXXXX")
94
+ trap 'rm -f "$tmp"' EXIT HUP INT TERM
95
+ # Group rules (notably Alpine's wheel rule) can match after the user
96
+ # specification and restore password authentication. The scoped
97
+ # Defaults entry makes the temporary NOPASSWD contract effective for the
98
+ # configured user while this file exists; it disappears on off/expiry.
99
+ printf 'Defaults:%s !authenticate\n%s ALL=(ALL) NOPASSWD: ALL\n' "$AGENT_USER" "$AGENT_USER" > "$tmp"
100
+ chmod 440 "$tmp"; chown root:"$(id -gn root)" "$tmp"
101
+ "$VISUDO" -cf "$tmp" >/dev/null
102
+ mv -f "$tmp" "$RULE"
103
+ trap - EXIT HUP INT TERM
104
+ }
105
+
106
+ write_state() {
107
+ activated_epoch=$1; expires_epoch=$2; ttl=$3; persist_reboot=$4
108
+ tmp=$(mktemp "${STATE}.XXXXXX")
109
+ trap 'rm -f "$tmp"' EXIT HUP INT TERM
110
+ {
111
+ printf 'version=%s\n' "$VERSION"
112
+ printf 'user=%s\n' "$AGENT_USER"
113
+ printf 'activated_at_epoch=%s\n' "$activated_epoch"
114
+ printf 'expires_at_epoch=%s\n' "$expires_epoch"
115
+ printf 'ttl=%s\n' "$ttl"
116
+ printf 'persist_reboot=%s\n' "$persist_reboot"
117
+ } > "$tmp"
118
+ chmod 600 "$tmp"; chown root:"$(id -gn root)" "$tmp"; mv -f "$tmp" "$STATE"
119
+ trap - EXIT HUP INT TERM
120
+ }
121
+
122
+ state_value() { sed -n "s/^$1=//p" "$STATE" | head -n 1; }
123
+
124
+ install_units() {
125
+ if [ "${INIT_SYSTEM:-}" = openrc ]; then
126
+ install -d -m 755 "$OPENRC_DIR"
127
+ install -o root -g "$(id -gn root)" -m 755 "$SOURCE_DIR/agent-temporary-boot.openrc" "$OPENRC_DIR/agent-temporary-boot"
128
+ install -o root -g "$(id -gn root)" -m 755 "$SOURCE_DIR/agent-temporary-expire.openrc" "$OPENRC_DIR/agent-temporary-expire"
129
+ "$RCUPDATE" add agent-temporary-boot default >/dev/null 2>&1 || true
130
+ "$RCUPDATE" add agent-temporary-expire default >/dev/null 2>&1 || true
131
+ return
132
+ fi
133
+ install -d -m 755 "$SYSTEMD_DIR"
134
+ install -o root -g "$(id -gn root)" -m 644 "$SOURCE_DIR/agent-temporary-expire.service" "$SYSTEMD_DIR/agent-temporary-expire.service"
135
+ install -o root -g "$(id -gn root)" -m 644 "$SOURCE_DIR/agent-temporary-expire.timer" "$SYSTEMD_DIR/agent-temporary-expire.timer"
136
+ install -o root -g "$(id -gn root)" -m 644 "$SOURCE_DIR/agent-temporary-boot.service" "$SYSTEMD_DIR/agent-temporary-boot.service"
137
+ "$SYSTEMCTL" daemon-reload
138
+ "$SYSTEMCTL" enable agent-temporary-boot.service >/dev/null
139
+ "$SYSTEMCTL" enable agent-temporary-expire.timer >/dev/null
140
+ }
141
+
142
+ do_install() {
143
+ need_root install; validate_platform
144
+ target_user=${INSTALL_USER:-${AGENT_USER:-${SUDO_USER:-}}}
145
+ [ -n "$target_user" ] || die "set AGENT_USER or run through sudo from the target user"
146
+ id "$target_user" >/dev/null 2>&1 || die "unknown user: $target_user"
147
+ install -d -m 755 "$PREFIX"; install -d -m 700 "$STATE_DIR"; install -d -m 700 "$STATE_DIR/backups"
148
+ stamp=$(date +%Y%m%d-%H%M%S); backup="$STATE_DIR/backups/$stamp"; install -d -m 700 "$backup"
149
+ for file in "$RULE" "$TARGET" "$CONFIG"; do [ -e "$file" ] && cp -p "$file" "$backup/$(basename "$file")" || true; done
150
+ rm -f "$RULE" "$STATE"
151
+ install -o root -g "$(id -gn root)" -m 755 "$0" "$TARGET"
152
+ tmp=$(mktemp "${CONFIG}.XXXXXX"); trap 'rm -f "$tmp"' EXIT HUP INT TERM
153
+ printf 'AGENT_USER=%s\n' "$target_user" > "$tmp"
154
+ chmod 600 "$tmp"; chown root:"$(id -gn root)" "$tmp"; mv -f "$tmp" "$CONFIG"
155
+ trap - EXIT HUP INT TERM
156
+ install_units
157
+ service_stop agent-temporary-expire
158
+ validate_sudoers
159
+ echo "Installed $TARGET version $VERSION for user $target_user; temporary access is inactive."
160
+ }
161
+
162
+ macos_plist_lint() {
163
+ command -v plutil >/dev/null 2>&1 || die "plutil is required on macOS"
164
+ plutil -lint "$1" >/dev/null || die "invalid launchd plist: $1"
165
+ }
166
+
167
+ macos_install_file() {
168
+ src=$1 dest=$2 mode=$3
169
+ [ -f "$src" ] || die "release file is missing: $src"
170
+ install -o root -g wheel -m "$mode" "$src" "$dest" || die "cannot install $dest"
171
+ }
172
+
173
+ do_install_macos() {
174
+ need_root install
175
+ command -v visudo >/dev/null 2>&1 || die "visudo is required"
176
+ [ ! -e /etc/sudoers.d/90-temporary-agent ] || die "legacy sudoers fragment exists; remove it before installation"
177
+ target_user=${INSTALL_USER:-${SUDO_USER:-}}
178
+ [ -n "$target_user" ] || die "set --user USER or run through sudo from the target user"
179
+ id "$target_user" >/dev/null 2>&1 || die "unknown user: $target_user"
180
+ macos_backend=$SOURCE_DIR/macos/agent-temporary-macos
181
+ macos_reaper=$SOURCE_DIR/macos/agent-temporary-reaper
182
+ macos_expire=$SOURCE_DIR/macos/com.agent-temporary.expire.plist
183
+ macos_boot=$SOURCE_DIR/macos/com.agent-temporary.boot.plist
184
+ for plist in "$macos_expire" "$macos_boot"; do macos_plist_lint "$plist"; done
185
+ sh -n "$macos_backend" "$macos_reaper"
186
+ if [ -e /etc/sudoers.d/90-agent-temporary ] || [ -e /var/db/agent-temporary/state ]; then
187
+ [ -x /usr/local/libexec/agent-temporary-macos ] || die "managed macOS authority exists but its backend is missing"
188
+ /usr/local/libexec/agent-temporary-macos off || die "cannot revoke existing macOS authority"
189
+ fi
190
+ "$VISUDO" -cf "$SUDOERS" >/dev/null || die "sudoers validation failed before installation"
191
+ install -d -o root -g wheel -m 755 /usr/local/bin /usr/local/libexec
192
+ install -d -o root -g wheel -m 700 /var/db/agent-temporary
193
+ macos_install_file "$macos_backend" /usr/local/libexec/agent-temporary-macos 755
194
+ macos_install_file "$macos_reaper" /usr/local/libexec/agent-temporary-reaper 755
195
+ macos_install_file "$macos_expire" /Library/LaunchDaemons/com.agent-temporary.expire.plist 644
196
+ macos_install_file "$macos_boot" /Library/LaunchDaemons/com.agent-temporary.boot.plist 644
197
+ tmp=$(mktemp /etc/agent-temporary.conf.XXXXXX)
198
+ printf 'CONFIG_USER=%s\n' "$target_user" > "$tmp"
199
+ chmod 600 "$tmp"; chown root:wheel "$tmp"; mv -f "$tmp" /etc/agent-temporary.conf
200
+ macos_install_file "$SOURCE_DIR/agent-temporary" "$TARGET" 755
201
+ launchctl bootout system/com.agent-temporary.expire >/dev/null 2>&1 || true
202
+ launchctl bootout system/com.agent-temporary.boot >/dev/null 2>&1 || true
203
+ launchctl bootstrap system /Library/LaunchDaemons/com.agent-temporary.expire.plist || die "cannot bootstrap expiry launchd job"
204
+ launchctl bootstrap system /Library/LaunchDaemons/com.agent-temporary.boot.plist || die "cannot bootstrap boot launchd job"
205
+ launchctl print system/com.agent-temporary.expire >/dev/null 2>&1 || die "expiry launchd job is not loaded"
206
+ launchctl print system/com.agent-temporary.boot >/dev/null 2>&1 || die "boot launchd job is not loaded"
207
+ "$VISUDO" -cf "$SUDOERS" >/dev/null || die "sudoers validation failed after installation"
208
+ echo "Installed $TARGET version $VERSION for user $target_user; temporary access is inactive."
209
+ }
210
+
211
+ do_on() {
212
+ need_root on
213
+ ttl_text=; persist_reboot=no
214
+ while [ "$#" -gt 0 ]; do
215
+ case "$1" in
216
+ --ttl) [ "$#" -ge 2 ] || die "--ttl requires a duration"; [ -z "$ttl_text" ] || die "--ttl supplied more than once"; ttl_text=$2; shift 2 ;;
217
+ --persist-reboot) [ "$persist_reboot" = no ] || die "--persist-reboot supplied more than once"; persist_reboot=yes; shift ;;
218
+ *) die "unknown on option: $1" ;;
219
+ esac
220
+ done
221
+ [ "$persist_reboot" = no ] || [ -n "$ttl_text" ] || die "--persist-reboot requires explicit --ttl DURATION"
222
+ [ -n "$ttl_text" ] || ttl_text=$DEFAULT_TTL
223
+ ttl_seconds=$(parse_ttl "$ttl_text"); validate_platform; load_user
224
+ if [ -f "$STATE" ]; then
225
+ expires=$(state_value expires_at_epoch); now=$(date +%s)
226
+ if [ -n "$expires" ] && [ "$now" -lt "$expires" ] && effective_unrestricted "$AGENT_USER"; then
227
+ echo "already active; expires_at_epoch=$expires; no changes made."; exit 0
228
+ fi
229
+ if [ -n "$expires" ] && [ "$now" -ge "$expires" ] && [ ! -e "$RULE" ]; then
230
+ rm -f "$STATE"
231
+ else
232
+ die "already active or inconsistent: run status/off"
233
+ fi
234
+ fi
235
+ now=$(date +%s); expires=$((now + ttl_seconds)); write_rule; write_state "$now" "$expires" "$ttl_text" "$persist_reboot"
236
+ if ! service_start agent-temporary-expire >/dev/null 2>&1; then rm -f "$RULE" "$STATE"; die "could not arm local expiry timer; privilege was revoked"; fi
237
+ validate_sudoers; echo "Temporary agent sudo access enabled for $AGENT_USER until epoch $expires."
238
+ }
239
+
240
+ do_off() {
241
+ need_root off; load_user; rm -f "$RULE" "$STATE"
242
+ service_stop agent-temporary-expire
243
+ validate_sudoers
244
+ if effective_unrestricted "$AGENT_USER"; then die "effective temporary privilege remains after revoke"; fi
245
+ echo "Temporary agent sudo access disabled."
246
+ }
247
+
248
+ do_expire() {
249
+ need_root internal; [ -f "$STATE" ] || exit 0; expires=$(state_value expires_at_epoch)
250
+ case "$expires" in ''|*[!0-9]*) rm -f "$RULE" "$STATE"; exit 0 ;; esac
251
+ [ "$(date +%s)" -ge "$expires" ] || exit 0; rm -f "$RULE"; validate_sudoers
252
+ }
253
+
254
+ do_expire_loop() {
255
+ need_root internal
256
+ while [ -f "$STATE" ]; do
257
+ expires=$(state_value expires_at_epoch); now=$(date +%s)
258
+ case "$expires" in ''|*[!0-9]*) rm -f "$RULE" "$STATE"; validate_sudoers; exit 0 ;; esac
259
+ if [ "$now" -ge "$expires" ]; then rm -f "$RULE"; validate_sudoers; exit 0; fi
260
+ sleep 1
261
+ done
262
+ }
263
+
264
+ do_boot_revoke() {
265
+ need_root internal
266
+ if [ ! -f "$STATE" ]; then rm -f "$RULE"; validate_sudoers; exit 0; fi
267
+ load_user; expires=$(state_value expires_at_epoch); now=$(date +%s); persist=$(state_value persist_reboot)
268
+ case "$expires" in ''|*[!0-9]*) rm -f "$RULE" "$STATE"; validate_sudoers; exit 0 ;; esac
269
+ if [ "$now" -ge "$expires" ]; then rm -f "$RULE" "$STATE"; service_stop agent-temporary-expire; validate_sudoers; exit 0; fi
270
+ if [ "$persist" != yes ]; then rm -f "$RULE" "$STATE"; service_stop agent-temporary-expire; validate_sudoers; exit 0; fi
271
+ if ! effective_unrestricted "$AGENT_USER"; then write_rule; fi
272
+ service_start agent-temporary-expire >/dev/null
273
+ validate_sudoers
274
+ }
275
+
276
+ do_status() {
277
+ if [ "$(id -u)" -ne 0 ] && { [ ! -r "$STATE" ] || [ ! -r "$RULE" ]; }; then
278
+ echo "status=inspection-requires-privilege"; echo "detail=root privileges are required to inspect effective temporary sudo state"; exit 0
279
+ fi
280
+ if [ ! -r "$CONFIG" ]; then echo "status=inspection-requires-privilege"; exit 0; fi
281
+ . "$CONFIG"; AGENT_USER=${AGENT_USER:-unknown}; rule_present=no; [ -e "$RULE" ] && rule_present=yes; state_present=no; [ -e "$STATE" ] && state_present=yes
282
+ if [ "$rule_present" = no ] && [ "$state_present" = no ]; then echo "state=inactive"; echo "effective_authority=false"; echo "user=$AGENT_USER"; echo "persist_reboot=no"; echo "version=$VERSION"; exit 0; fi
283
+ if [ "$rule_present" = yes ] && [ "$state_present" = no ]; then echo "state=inconsistent"; echo "effective_authority=unknown"; echo "status=broken/inconsistent"; echo "detail=effective rule without state"; exit 0; fi
284
+ if [ "$rule_present" = no ] && [ "$state_present" = yes ]; then
285
+ expires=$(state_value expires_at_epoch); now=$(date +%s)
286
+ if [ -n "$expires" ] && [ "$now" -ge "$expires" ]; then echo "state=expired"; echo "effective_authority=false"; else echo "state=inconsistent"; echo "effective_authority=false"; echo "status=broken/inconsistent"; echo "detail=state without effective rule"; fi
287
+ exit 0
288
+ fi
289
+ expires=$(state_value expires_at_epoch); now=$(date +%s)
290
+ if [ -z "$expires" ]; then echo "state=inconsistent"; echo "effective_authority=unknown"; echo "status=broken/inconsistent"; echo "detail=missing expiry"; exit 0; fi
291
+ if [ "$now" -ge "$expires" ]; then
292
+ if effective_unrestricted "$AGENT_USER"; then echo "state=inconsistent"; echo "effective_authority=true"; echo "status=security-error"; echo "detail=expiry passed but privilege remains";
293
+ else echo "state=expired"; echo "effective_authority=false"; fi
294
+ exit 0
295
+ fi
296
+ if ! effective_unrestricted "$AGENT_USER"; then echo "state=inconsistent"; echo "effective_authority=false"; echo "status=broken/inconsistent"; echo "detail=exact sudo execution is not effective"; exit 0; fi
297
+ if ! service_active agent-temporary-expire; then echo "state=inconsistent"; echo "effective_authority=true"; echo "status=security-error"; echo "detail=expiry timer is not active"; exit 0; fi
298
+ persist=$(state_value persist_reboot); [ "$persist" = yes ] || persist=no
299
+ remaining=$((expires - now)); echo "state=active"; echo "effective_authority=true"; echo "user=$AGENT_USER"; echo "version=$(state_value version)"; echo "activated_at_epoch=$(state_value activated_at_epoch)"; echo "expires_at_epoch=$expires"; echo "remaining_seconds=$remaining"; echo "ttl=$(state_value ttl)"; echo "persist_reboot=$persist"; echo "scope=unrestricted-root"; echo "authentication=NOPASSWD"
300
+ }
301
+
302
+ if [ "$OS" = Darwin ]; then
303
+ case "${1:-status}" in
304
+ install) shift; INSTALL_USER=; [ "${1:-}" = --user ] && { [ "$#" -eq 2 ] || usage; INSTALL_USER=$2; }; do_install_macos ;;
305
+ version) [ "$#" -eq 1 ] || usage; echo "agent-temporary $VERSION" ;;
306
+ --validate-ttl) [ "$#" -eq 2 ] || usage; parse_ttl "$2" >/dev/null; echo valid ;;
307
+ *) exec /usr/local/libexec/agent-temporary-macos "$@" ;;
308
+ esac
309
+ exit 0
310
+ fi
311
+
312
+ case "${1:-status}" in
313
+ on) shift; do_on "$@" ;; off) do_off ;; status)
314
+ shift; STATUS_JSON=no
315
+ if [ "$#" -eq 1 ] && [ "$1" = --json ]; then STATUS_JSON=yes
316
+ elif [ "$#" -ne 0 ]; then usage
317
+ fi
318
+ if [ "$STATUS_JSON" = yes ]; then
319
+ do_status | awk -F= 'BEGIN { printf "{"; first=1 }
320
+ { key=$1; value=substr($0, index($0,"=")+1); gsub(/\\/,"\\\\",value); gsub(/"/,"\\\"",value); if (!first) printf ","; printf "\"%s\":\"%s\"",key,value; first=0 }
321
+ END { print "}" }'
322
+ else do_status
323
+ fi ;;
324
+ version) version ;;
325
+ --validate-ttl) [ "$#" -eq 2 ] || usage; parse_ttl "$2" >/dev/null; echo valid ;;
326
+ --internal-expire) do_expire ;; --internal-expire-loop) do_expire_loop ;; --internal-boot-revoke) do_boot_revoke ;; *) usage ;;
327
+ esac
@@ -0,0 +1,10 @@
1
+ #!/sbin/openrc-run
2
+
3
+ name="agent-temporary-boot"
4
+ command="/usr/local/bin/agent-temporary"
5
+ command_args="--internal-boot-revoke"
6
+
7
+ depend() {
8
+ need localmount
9
+ after bootmisc
10
+ }
@@ -0,0 +1,11 @@
1
+ [Unit]
2
+ Description=Revoke agent-temporary privilege after boot
3
+ After=local-fs.target
4
+ Before=sshd.service
5
+
6
+ [Service]
7
+ Type=oneshot
8
+ ExecStart=/usr/local/bin/agent-temporary --internal-boot-revoke
9
+
10
+ [Install]
11
+ WantedBy=multi-user.target
@@ -0,0 +1,15 @@
1
+ #!/sbin/openrc-run
2
+
3
+ name="agent-temporary-expire"
4
+ command="/usr/local/bin/agent-temporary"
5
+ command_args="--internal-expire-loop"
6
+ command_background=true
7
+ supervisor="supervise-daemon"
8
+ respawn_delay=30
9
+ respawn_max=5
10
+ respawn_period=600
11
+
12
+ depend() {
13
+ need localmount
14
+ after bootmisc
15
+ }
@@ -0,0 +1,6 @@
1
+ [Unit]
2
+ Description=Expire agent-temporary privilege when its local TTL ends
3
+
4
+ [Service]
5
+ Type=oneshot
6
+ ExecStart=/usr/local/bin/agent-temporary --internal-expire
@@ -0,0 +1,11 @@
1
+ [Unit]
2
+ Description=Local expiry check for agent-temporary privilege
3
+
4
+ [Timer]
5
+ OnBootSec=1min
6
+ OnUnitActiveSec=1min
7
+ AccuracySec=1s
8
+ Unit=agent-temporary-expire.service
9
+
10
+ [Install]
11
+ WantedBy=timers.target
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ set -eu
3
+ base=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
4
+ exec env AGENT_TEMPORARY_RELEASE_DIR="$base" sh "$base/agent-temporary" install "$@"
@@ -0,0 +1,292 @@
1
+ #!/bin/sh
2
+
3
+ VERSION=0.7.0
4
+ RULE=${AGENT_TEMPORARY_RULE:-/etc/sudoers.d/90-agent-temporary}
5
+ STATE_DIR=${AGENT_TEMPORARY_STATE_DIR:-/var/db/agent-temporary}
6
+ STATE=${AGENT_TEMPORARY_STATE:-$STATE_DIR/state}
7
+ LOCK=${AGENT_TEMPORARY_LOCK:-$STATE_DIR/.lock}
8
+ SUDOERS=${AGENT_TEMPORARY_SUDOERS:-/etc/sudoers}
9
+ CONFIG=${AGENT_TEMPORARY_CONFIG:-/etc/agent-temporary.conf}
10
+ EXPIRY_LABEL=com.agent-temporary.expire
11
+
12
+ ID=${ID:-/usr/bin/id}
13
+ DATE=${DATE:-/bin/date}
14
+ SYSCTL=${SYSCTL:-/usr/sbin/sysctl}
15
+ MKTEMP=${MKTEMP:-/usr/bin/mktemp}
16
+ MV=${MV:-/bin/mv}
17
+ RM=${RM:-/bin/rm}
18
+ MKDIR=${MKDIR:-/bin/mkdir}
19
+ RMDIR=${RMDIR:-/bin/rmdir}
20
+ CHMOD=${CHMOD:-/bin/chmod}
21
+ CHOWN=${CHOWN:-/usr/sbin/chown}
22
+ VISUDO=${VISUDO:-/usr/sbin/visudo}
23
+ LAUNCHCTL=${LAUNCHCTL:-/bin/launchctl}
24
+ PROBE=${PROBE:-}
25
+ SUDO=${SUDO:-/usr/bin/sudo}
26
+ SU=${SU:-/usr/bin/su}
27
+ TRUE=${TRUE:-/usr/bin/true}
28
+ KICK_ATTEMPTS=${KICK_ATTEMPTS:-100}
29
+ KICK_SLEEP=${KICK_SLEEP:-0.1}
30
+ REAPER_LOCK_ATTEMPTS=${REAPER_LOCK_ATTEMPTS:-100}
31
+ REAPER_LOCK_SLEEP=${REAPER_LOCK_SLEEP:-0.1}
32
+
33
+ die() { echo "agent-temporary: $*" >&2; exit 1; }
34
+ usage() { echo "usage: agent-temporary-macos on [--ttl MINUTESm|HOURSh] [--persist-reboot] | off | status | --reaper | --boot-revoke" >&2; exit 2; }
35
+
36
+ require_root() { [ "$($ID -u 2>/dev/null)" = 0 ] || die "root is required"; }
37
+
38
+ load_user() {
39
+ TARGET_USER=${AGENT_TEMPORARY_USER:-}
40
+ if [ -r "$CONFIG" ]; then
41
+ CONFIG_USER=
42
+ . "$CONFIG"
43
+ [ -n "$CONFIG_USER" ] && TARGET_USER=$CONFIG_USER
44
+ fi
45
+ [ -n "$TARGET_USER" ] || die "configured target user is required"
46
+ "$ID" "$TARGET_USER" >/dev/null 2>&1 || die "unknown target user: $TARGET_USER"
47
+ }
48
+
49
+ now() { "$DATE" +%s; }
50
+ boot_identity() {
51
+ raw=$("$SYSCTL" -n kern.boottime 2>/dev/null) || return 1
52
+ sec=$(printf '%s\n' "$raw" | sed -n 's/.*{[[:space:]]*sec[[:space:]]*=[[:space:]]*\([0-9][0-9]*\).*/\1/p' | sed -n '1p')
53
+ usec=$(printf '%s\n' "$raw" | sed -n 's/.*,[[:space:]]*usec[[:space:]]*=[[:space:]]*\([0-9][0-9]*\).*/\1/p' | sed -n '1p')
54
+ digits "$sec" && digits "$usec" || return 1
55
+ printf '%s:%s\n' "$sec" "$usec"
56
+ }
57
+ state_value() { [ -f "$STATE" ] || return 1; sed -n "s/^$1=//p" "$STATE" | sed -n '1p'; }
58
+ digits() { case $1 in ''|*[!0-9]*) return 1;; *) return 0;; esac; }
59
+
60
+ parse_ttl() {
61
+ ttl=$1
62
+ case "$ttl" in
63
+ [0-9]*m) number=${ttl%m}; unit=m;;
64
+ [0-9]*h) number=${ttl%h}; unit=h;;
65
+ *) die "TTL must be a positive integer number of minutes or hours";;
66
+ esac
67
+ digits "$number" || die "malformed TTL: $ttl"
68
+ while [ "${number#0}" != "$number" ]; do number=${number#0}; done
69
+ [ -n "$number" ] || die "TTL must be greater than zero"
70
+ case "$unit" in
71
+ m) [ "$number" -le 480 ] || die "TTL maximum is 8h"; seconds=$((number * 60));;
72
+ h) [ "$number" -le 8 ] || die "TTL maximum is 8h"; seconds=$((number * 3600));;
73
+ esac
74
+ [ "$seconds" -ge 300 ] || die "TTL minimum is 5m"
75
+ echo "$seconds"
76
+ }
77
+
78
+ valid_state() {
79
+ [ -f "$STATE" ] || return 1
80
+ awk -F= '
81
+ BEGIN { ok=1; count=0 }
82
+ NF != 2 { ok=0; next }
83
+ $1 !~ /^(phase|user|issued_at|expires_at|boot_identity|persist_reboot)$/ { ok=0 }
84
+ { seen[$1]++; if (seen[$1] > 1) ok=0 }
85
+ { count++ }
86
+ END {
87
+ if (count != 6) ok=0
88
+ if (!seen["phase"] || !seen["user"] || !seen["issued_at"] || !seen["expires_at"] || !seen["boot_identity"] || !seen["persist_reboot"]) ok=0
89
+ exit(ok ? 0 : 1)
90
+ }
91
+ ' "$STATE" 2>/dev/null || return 1
92
+ phase=$(state_value phase) || return 1
93
+ user=$(state_value user) || return 1
94
+ issued=$(state_value issued_at) || return 1
95
+ expires=$(state_value expires_at) || return 1
96
+ boot=$(state_value boot_identity) || return 1
97
+ persist=$(state_value persist_reboot) || return 1
98
+ [ "$phase" = active ] || [ "$phase" = prepared ] || return 1
99
+ [ -n "$user" ] && digits "$issued" && digits "$expires" && [ "$issued" -le "$expires" ] && [ -n "$boot" ] && { [ "$persist" = yes ] || [ "$persist" = no ]; }
100
+ }
101
+
102
+ secure_state_dir() {
103
+ "$MKDIR" -p "$STATE_DIR" 2>/dev/null || die "cannot create state directory"
104
+ "$CHMOD" 700 "$STATE_DIR" 2>/dev/null || die "cannot secure state directory"
105
+ "$CHOWN" root:wheel "$STATE_DIR" 2>/dev/null || die "cannot secure state ownership"
106
+ }
107
+ acquire_lock() {
108
+ secure_state_dir
109
+ "$MKDIR" "$LOCK" 2>/dev/null || die "transaction already in progress"
110
+ "$CHMOD" 700 "$LOCK" 2>/dev/null || die "cannot secure transaction lock"
111
+ "$CHOWN" root:wheel "$LOCK" 2>/dev/null || die "cannot secure transaction lock ownership"
112
+ trap release_lock EXIT INT TERM
113
+ }
114
+ acquire_lock_wait() {
115
+ secure_state_dir
116
+ attempts=0
117
+ while ! "$MKDIR" "$LOCK" 2>/dev/null; do
118
+ [ "$attempts" -lt "$REAPER_LOCK_ATTEMPTS" ] || die "reaper transaction lock wait expired"
119
+ attempts=$((attempts + 1)); sleep "$REAPER_LOCK_SLEEP"
120
+ done
121
+ "$CHMOD" 700 "$LOCK" 2>/dev/null || die "cannot secure transaction lock"
122
+ "$CHOWN" root:wheel "$LOCK" 2>/dev/null || die "cannot secure transaction lock ownership"
123
+ trap release_lock EXIT INT TERM
124
+ }
125
+ release_lock() { "$RMDIR" "$LOCK" 2>/dev/null || true; trap - EXIT INT TERM; }
126
+ remove_rule() { [ -e "$RULE" ] && "$RM" -f "$RULE"; }
127
+ clear_state() { [ -e "$STATE" ] && "$RM" -f "$STATE"; }
128
+ validate_full() { "$VISUDO" -cf "$SUDOERS" >/dev/null 2>&1; }
129
+
130
+ exact_probe() {
131
+ if [ -n "$PROBE" ]; then "$PROBE" "$1"; else "$SU" "$1" -c "$SUDO -k; $SUDO -n $TRUE"; fi
132
+ }
133
+
134
+ daemon_running() {
135
+ "$LAUNCHCTL" print "system/$EXPIRY_LABEL" 2>/dev/null | grep -q 'state = running'
136
+ }
137
+
138
+ kick_reaper() {
139
+ "$LAUNCHCTL" print "system/$EXPIRY_LABEL" >/dev/null 2>&1 || return 1
140
+ "$LAUNCHCTL" kickstart -p "system/$EXPIRY_LABEL" >/dev/null 2>&1 || return 1
141
+ i=0
142
+ while [ "$i" -lt "$KICK_ATTEMPTS" ]; do
143
+ daemon_running && return 0
144
+ i=$((i + 1)); sleep "$KICK_SLEEP"
145
+ done
146
+ return 1
147
+ }
148
+
149
+ write_state() {
150
+ phase=$1 user=$2 issued=$3 expires=$4 boot=$5 persist=$6
151
+ tmp=$($MKTEMP "$STATE.XXXXXX") || return 1
152
+ if ! { printf 'phase=%s\n' "$phase"; printf 'user=%s\n' "$user"; printf 'issued_at=%s\n' "$issued"; printf 'expires_at=%s\n' "$expires"; printf 'boot_identity=%s\n' "$boot"; printf 'persist_reboot=%s\n' "$persist"; } >"$tmp"; then
153
+ "$RM" -f "$tmp"; return 1
154
+ fi
155
+ "$CHMOD" 600 "$tmp" 2>/dev/null || { "$RM" -f "$tmp"; return 1; }
156
+ "$CHOWN" root:wheel "$tmp" 2>/dev/null || { "$RM" -f "$tmp"; return 1; }
157
+ "$MV" "$tmp" "$STATE" 2>/dev/null || { "$RM" -f "$tmp"; return 1; }
158
+ }
159
+
160
+ write_candidate() {
161
+ user=$1
162
+ tmp=$($MKTEMP "$RULE.XXXXXX") || return 1
163
+ if ! { printf 'Defaults:%s !authenticate\n' "$user"; printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$user"; } >"$tmp"; then
164
+ "$RM" -f "$tmp"; return 1
165
+ fi
166
+ "$CHMOD" 440 "$tmp" 2>/dev/null || { "$RM" -f "$tmp"; return 1; }
167
+ "$CHOWN" root:wheel "$tmp" 2>/dev/null || { "$RM" -f "$tmp"; return 1; }
168
+ "$VISUDO" -cf "$tmp" >/dev/null 2>&1 || { "$RM" -f "$tmp"; return 1; }
169
+ CANDIDATE=$tmp
170
+ }
171
+
172
+ rollback() {
173
+ [ -n "${CANDIDATE:-}" ] && "$RM" -f "$CANDIDATE" 2>/dev/null || true
174
+ remove_rule; clear_state; validate_full || true
175
+ }
176
+
177
+ revoke_locked() {
178
+ user=${1:-}; validation_failed=0
179
+ remove_rule
180
+ validate_full || validation_failed=1
181
+ clear_state
182
+ if [ -n "$user" ] && exact_probe "$user" >/dev/null 2>&1; then probe_failed=1; else probe_failed=0; fi
183
+ [ "$validation_failed" = 0 ] && [ "$probe_failed" = 0 ]
184
+ }
185
+
186
+ classify_before_on() {
187
+ if [ -e "$RULE" ] && ! valid_state; then revoke_locked "${TARGET_USER:-}" || true; return 0; fi
188
+ if [ -f "$STATE" ] && [ ! -e "$RULE" ]; then clear_state; return 0; fi
189
+ if valid_state; then
190
+ phase=$(state_value phase); expires=$(state_value expires_at); existing_persist=$(state_value persist_reboot)
191
+ if [ "$phase" != active ] || [ "$expires" -le "$(now)" ]; then
192
+ revoke_locked "$(state_value user)" || true
193
+ elif [ -e "$RULE" ]; then
194
+ if exact_probe "$(state_value user)" >/dev/null 2>&1 && daemon_running; then
195
+ [ "$existing_persist" = "$REQUESTED_PERSIST" ] || die "already active; run off before changing persistence"
196
+ echo "already active; expires_at=$expires; no changes made."; exit 0
197
+ fi
198
+ revoke_locked "$(state_value user)" || true
199
+ fi
200
+ fi
201
+ }
202
+
203
+ on_cmd() {
204
+ require_root; load_user; ttl=300; REQUESTED_PERSIST=no
205
+ while [ "$#" -gt 0 ]; do
206
+ case $1 in
207
+ --ttl) [ "$#" -ge 2 ] || usage; ttl=$(parse_ttl "$2") || exit 1; shift 2;;
208
+ --persist-reboot) [ "$REQUESTED_PERSIST" = no ] || usage; REQUESTED_PERSIST=yes; shift;;
209
+ *) usage;;
210
+ esac
211
+ done
212
+ acquire_lock; classify_before_on
213
+ started=$(now); expires=$((started + ttl)); boot=$(boot_identity) || { release_lock; die "cannot determine boot identity"; }
214
+ write_candidate "$TARGET_USER" || { rollback; release_lock; die "cannot prepare sudoers candidate"; }
215
+ write_state prepared "$TARGET_USER" "$started" "$expires" "$boot" "$REQUESTED_PERSIST" || { rollback; release_lock; die "cannot write prepared state"; }
216
+ "$MV" "$CANDIDATE" "$RULE" 2>/dev/null || { rollback; release_lock; die "cannot install sudoers fragment"; }
217
+ CANDIDATE=
218
+ validate_full || { rollback; release_lock; die "complete sudoers validation failed"; }
219
+ exact_probe "$TARGET_USER" >/dev/null 2>&1 || { rollback; release_lock; die "exact authority probe failed"; }
220
+ write_state active "$TARGET_USER" "$started" "$expires" "$boot" "$REQUESTED_PERSIST" || { rollback; release_lock; die "cannot commit active state"; }
221
+ kick_reaper || { rollback; release_lock; die "expiry launchd job is not ready"; }
222
+ echo "Temporary agent access enabled for $TARGET_USER until epoch $expires."
223
+ }
224
+
225
+ status_cmd() {
226
+ if [ "$($ID -u 2>/dev/null)" -ne 0 ]; then
227
+ if [ ! -e "$RULE" ] && [ ! -e "$STATE" ]; then
228
+ echo 'state=inactive'; echo 'effective_authority=false'; echo "version=$VERSION"; return 0
229
+ fi
230
+ echo 'status=inspection-requires-privilege'
231
+ echo 'detail=root privileges are required to inspect effective temporary sudo state'
232
+ return 0
233
+ fi
234
+ acquire_lock
235
+ if [ ! -e "$RULE" ] && [ ! -e "$STATE" ]; then echo 'state=inactive'; echo 'effective_authority=false'; echo "version=$VERSION"; return 0; fi
236
+ if ! valid_state; then remove_rule; clear_state; validate_full || true; echo 'state=inconsistent'; echo 'detail=orphan-or-invalid-authority'; echo 'effective_authority=false'; echo "version=$VERSION"; return 0; fi
237
+ user=$(state_value user); expires=$(state_value expires_at)
238
+ if [ ! -e "$RULE" ]; then clear_state; echo 'state=inconsistent'; echo 'detail=state-without-authority'; echo 'effective_authority=false'; echo "version=$VERSION"; return 0; fi
239
+ if [ "$expires" -le "$(now)" ]; then revoke_locked "$user" || true; echo 'state=expired'; echo 'effective_authority=false'; echo "expires_at=$expires"; echo "version=$VERSION"; return 0; fi
240
+ if ! daemon_running; then revoke_locked "$user" || true; echo 'state=inconsistent'; echo 'detail=reaper-unavailable'; echo 'effective_authority=false'; echo "version=$VERSION"; return 0; fi
241
+ if ! exact_probe "$user" >/dev/null 2>&1; then revoke_locked "$user" || true; echo 'state=inconsistent'; echo 'detail=effective-authority-failed'; echo 'effective_authority=false'; echo "version=$VERSION"; return 0; fi
242
+ echo 'state=active'; echo 'effective_authority=true'; echo "user=$user"; echo "issued_at=$(state_value issued_at)"; echo "expires_at=$expires"; echo "remaining_seconds=$((expires - $(now)))"; echo "persist_reboot=$(state_value persist_reboot)"; echo "version=$VERSION"
243
+ }
244
+
245
+ off_cmd() {
246
+ require_root; user=${AGENT_TEMPORARY_USER:-}; [ -f "$STATE" ] && user=$(state_value user 2>/dev/null || printf '%s' "$user"); acquire_lock_wait
247
+ revoke_locked "$user" || { release_lock; die "revocation validation or exact probe failed"; }
248
+ echo 'Temporary agent access disabled.'
249
+ }
250
+
251
+ reaper_cmd() {
252
+ require_root; acquire_lock_wait
253
+ if ! valid_state || [ ! -e "$RULE" ]; then revoke_locked "$(state_value user 2>/dev/null || true)" || true; release_lock; exit 0; fi
254
+ user=$(state_value user); expires=$(state_value expires_at); boot=$(state_value boot_identity); current_boot=$(boot_identity)
255
+ if [ "$expires" -le "$(now)" ] || [ "$boot" != "$current_boot" ]; then revoke_locked "$user" || true; release_lock; exit 0; fi
256
+ release_lock
257
+ while :; do
258
+ [ -f "$STATE" ] && [ -e "$RULE" ] || exit 0
259
+ [ "$(now)" -ge "$expires" ] && { acquire_lock; revoke_locked "$user" || true; release_lock; exit 0; }
260
+ sleep 1
261
+ done
262
+ }
263
+
264
+ boot_reconcile_cmd() {
265
+ require_root; acquire_lock
266
+ if [ ! -f "$STATE" ]; then remove_rule; validate_full || true; return 0; fi
267
+ if ! valid_state; then revoke_locked "$(state_value user 2>/dev/null || true)" || true; return 1; fi
268
+ phase=$(state_value phase); user=$(state_value user); issued=$(state_value issued_at); expires=$(state_value expires_at); persist=$(state_value persist_reboot); current_boot=$(boot_identity)
269
+ if [ "$phase" != active ] || [ "$persist" != yes ] || [ "$expires" -le "$(now)" ] || [ "$(now)" -lt "$issued" ]; then
270
+ revoke_locked "$user" || true; return 0
271
+ fi
272
+ if [ -z "$current_boot" ]; then revoke_locked "$user" || true; return 1; fi
273
+ CANDIDATE=
274
+ write_candidate "$user" || { rollback; return 1; }
275
+ "$MV" "$CANDIDATE" "$RULE" 2>/dev/null || { rollback; return 1; }
276
+ CANDIDATE=
277
+ validate_full || { rollback; return 1; }
278
+ exact_probe "$user" >/dev/null 2>&1 || { rollback; return 1; }
279
+ write_state active "$user" "$issued" "$expires" "$current_boot" yes || { rollback; return 1; }
280
+ kick_reaper || { rollback; return 1; }
281
+ return 0
282
+ }
283
+
284
+ case ${1:-} in
285
+ on) shift; on_cmd "$@";;
286
+ off) shift; [ "$#" -eq 0 ] || usage; off_cmd;;
287
+ status) shift; [ "$#" -eq 0 ] || usage; status_cmd;;
288
+ --reaper) [ "$#" -eq 1 ] || usage; reaper_cmd;;
289
+ --boot-revoke) [ "$#" -eq 1 ] || usage; boot_reconcile_cmd;;
290
+ version|--version) [ "$#" -eq 1 ] || usage; echo "agent-temporary $VERSION";;
291
+ *) usage;;
292
+ esac
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ exec /usr/local/libexec/agent-temporary-macos --reaper
@@ -0,0 +1,22 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>com.agent-temporary.boot</string>
7
+ <key>ProgramArguments</key>
8
+ <array>
9
+ <string>/usr/local/libexec/agent-temporary-macos</string>
10
+ <string>--boot-revoke</string>
11
+ </array>
12
+ <key>RunAtLoad</key>
13
+ <true/>
14
+ <key>KeepAlive</key>
15
+ <dict>
16
+ <key>SuccessfulExit</key>
17
+ <false/>
18
+ </dict>
19
+ <key>ThrottleInterval</key>
20
+ <integer>60</integer>
21
+ </dict>
22
+ </plist>
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>Label</key>
6
+ <string>com.agent-temporary.expire</string>
7
+ <key>ProgramArguments</key>
8
+ <array>
9
+ <string>/usr/local/libexec/agent-temporary-reaper</string>
10
+ <string>--reaper</string>
11
+ </array>
12
+ <key>ThrottleInterval</key>
13
+ <integer>60</integer>
14
+ </dict>
15
+ </plist>
@@ -0,0 +1,52 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ PREFIX=${PREFIX:-/usr/local/bin}
5
+ TARGET=${TARGET:-$PREFIX/agent-temporary}
6
+ UNAME=${UNAME:-uname}
7
+ OS=$($UNAME -s 2>/dev/null || true)
8
+ if [ "$OS" = Darwin ]; then
9
+ RULE=${AGENT_TEMPORARY_RULE:-/etc/sudoers.d/90-agent-temporary}
10
+ LEGACY_RULE=${AGENT_TEMPORARY_LEGACY_RULE:-/etc/sudoers.d/90-temporary-agent}
11
+ STATE_DIR=${AGENT_TEMPORARY_STATE_DIR:-/var/db/agent-temporary}
12
+ [ "$(id -u)" -eq 0 ] || { echo "Run as root: sudo $0" >&2; exit 1; }
13
+ [ ! -e "$LEGACY_RULE" ] || { echo "Security error: legacy sudoers authority fragment exists at $LEGACY_RULE; inspect and intentionally clean it up before uninstall" >&2; exit 1; }
14
+ [ -x /usr/local/libexec/agent-temporary-macos ] || { [ ! -e "$RULE" ] || { echo "macOS backend is missing; refusing unsafe uninstall" >&2; exit 1; }; }
15
+ if [ -e "$RULE" ] || [ -e "$STATE_DIR/state" ]; then
16
+ /usr/local/libexec/agent-temporary-macos off || { echo "Could not revoke managed macOS authority" >&2; exit 1; }
17
+ fi
18
+ /usr/sbin/visudo -cf /etc/sudoers >/dev/null || { echo "sudoers validation failed after revoke" >&2; exit 1; }
19
+ /bin/launchctl bootout system/com.agent-temporary.expire >/dev/null 2>&1 || true
20
+ /bin/launchctl bootout system/com.agent-temporary.boot >/dev/null 2>&1 || true
21
+ rm -f /Library/LaunchDaemons/com.agent-temporary.expire.plist /Library/LaunchDaemons/com.agent-temporary.boot.plist /usr/local/libexec/agent-temporary-macos /usr/local/libexec/agent-temporary-reaper "$TARGET" /etc/agent-temporary.conf
22
+ rm -rf "$STATE_DIR"
23
+ /usr/sbin/visudo -cf /etc/sudoers >/dev/null || { echo "sudoers validation failed after uninstall" >&2; exit 1; }
24
+ echo "Removed macOS agent-temporary runtime, privilege rule, launchd jobs, and state."
25
+ exit 0
26
+ fi
27
+ RULE=${AGENT_TEMPORARY_RULE:-/etc/sudoers.d/90-temporary-agent}
28
+ CONFIG=${AGENT_TEMPORARY_CONFIG:-/etc/agent-temporary.conf}
29
+ STATE_DIR=${AGENT_TEMPORARY_STATE_DIR:-/var/lib/agent-temporary}
30
+ SYSTEMD_DIR=${AGENT_TEMPORARY_SYSTEMD_DIR:-/etc/systemd/system}
31
+ OPENRC_DIR=${AGENT_TEMPORARY_OPENRC_DIR:-/etc/init.d}
32
+ SYSTEMCTL=${SYSTEMCTL:-systemctl}
33
+ RCSERVICE=${RCSERVICE:-/sbin/rc-service}
34
+ RCUPDATE=${RCUPDATE:-/sbin/rc-update}
35
+
36
+ [ "$(id -u)" -eq 0 ] || { echo "Run as root: sudo $0" >&2; exit 1; }
37
+ rm -f "$RULE"
38
+ if command -v "$SYSTEMCTL" >/dev/null 2>&1 && { [ -d /run/systemd/system ] || "$SYSTEMCTL" show-environment >/dev/null 2>&1; }; then
39
+ "$SYSTEMCTL" disable --now agent-temporary-expire.timer >/dev/null 2>&1 || true
40
+ rm -f "$SYSTEMD_DIR/agent-temporary-expire.service" "$SYSTEMD_DIR/agent-temporary-expire.timer" "$SYSTEMD_DIR/agent-temporary-boot.service"
41
+ "$SYSTEMCTL" daemon-reload >/dev/null 2>&1 || true
42
+ elif [ -x "$RCSERVICE" ]; then
43
+ "$RCSERVICE" agent-temporary-expire stop >/dev/null 2>&1 || true
44
+ "$RCUPDATE" del agent-temporary-boot default >/dev/null 2>&1 || true
45
+ "$RCUPDATE" del agent-temporary-expire default >/dev/null 2>&1 || true
46
+ "$RCSERVICE" agent-temporary-boot stop >/dev/null 2>&1 || true
47
+ rm -f "$OPENRC_DIR/agent-temporary-boot" "$OPENRC_DIR/agent-temporary-expire"
48
+ fi
49
+ rm -f "$CONFIG" "$TARGET"
50
+ rm -rf "$STATE_DIR"
51
+ if command -v visudo >/dev/null 2>&1; then visudo -cf /etc/sudoers >/dev/null; fi
52
+ echo "Removed agent-temporary runtime, privilege rule, service integration, and state."