Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 214 additions & 5 deletions README.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/Effect/ContextFacts.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ public function __construct(
/** A stable digest of the facts, so a receipt can cite exactly what was judged. */
public function fingerprint(): string
{
// By the bytes, never the way PHP compares what looks like a number: «1.0» and «1.00» are two scopes.
$scopes = $this->scopes;
sort($scopes);
sort($scopes, \SORT_STRING);

return 'sha256:' . hash('sha256', (string) json_encode([
'channel' => $this->channel,
Expand Down
4 changes: 2 additions & 2 deletions src/Effect/DeclaredAuthorityPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ public function digest(): string
$reglas = [];
foreach ($this->rules as $operacion => $regla) {
$scopes = $regla['scopes'];
sort($scopes);
sort($scopes, \SORT_STRING);
$reglas[$operacion] = ['authority' => $regla['authority']->value, 'scopes' => $scopes];
}
ksort($reglas);
ksort($reglas, \SORT_STRING);

return 'sha256:' . hash('sha256', (string) json_encode(['id' => $this->id, 'rules' => $reglas]));
}
Expand Down
8 changes: 5 additions & 3 deletions src/Effect/DescentCertificate.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,14 @@ public function canonicalPayload(): string
'subject' => $p->subject->value,
];

// By the bytes of the names, never the way PHP compares what looks like a number: two keys that compare
// equal as numbers kept the order they were written in, and one certificate had two spellings to sign.
$predicado = $this->predicate;
ksort($predicado);
ksort($predicado, \SORT_STRING);
$sobre = $this->envelope;
$this->ordenaProfundo($sobre);
$cubre = $this->covers;
sort($cubre);
sort($cubre, \SORT_STRING);

return (string) json_encode([
'certificate_subject' => [
Expand Down Expand Up @@ -195,7 +197,7 @@ public function coversAll(array $loweredAxes): bool
/** @param array<string, mixed> $bag */
private function ordenaProfundo(array &$bag): void
{
ksort($bag);
ksort($bag, \SORT_STRING);
foreach ($bag as &$v) {
if (\is_array($v) && ! array_is_list($v)) {
/** @var array<string, mixed> $v */
Expand Down
41 changes: 29 additions & 12 deletions src/Effect/EffectProfile.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
namespace Milpa\Command\Effect;

use Milpa\Command\Consent\OperationId;
use Milpa\Command\Standing\Canonical;

/**
* What an operation can do at WORST — the upper bound, declared by the operation itself.
Expand Down Expand Up @@ -53,6 +54,15 @@
*/
final class EffectProfile
{
/**
* The rollback contract, when reversibility claims to be `Guaranteed`.
*
* Required for that one level and for no other, because `Guaranteed` is the only claim that
* buys the operation LESS scrutiny. A claim that lowers controls has to name the artifact
* that backs it, or it is exactly the self-certification GOV-00 exists to forbid.
*/
public readonly ?string $rollbackContract;

/**
* @param list<string> $escalatesOn argument names whose value can RAISE this ceiling — declared
* so a caller knows which unresolved argument is the one that
Expand All @@ -73,13 +83,15 @@ public function __construct(
*/
public readonly Subject $subject = Subject::Unknown,
/**
* The rollback contract, when reversibility claims to be `Guaranteed`.
* The rollback contract ({@see self::$rollbackContract}): text, or `null`.
*
* Required for that one level and for no other, because `Guaranteed` is the only claim that
* buys the operation LESS scrutiny. A claim that lowers controls has to name the artifact
* that backs it, or it is exactly the self-certification GOV-00 exists to forbid.
* THE TYPE IS ASKED HERE, NOT LEFT TO THE CALLER'S FILE (greenhouse decisions/0615, a ninth review).
* A standing grant holds the inverse its ceiling names to «the name of one operation, as it reads», and
* it reads it from here. From PHP's default mode an `OperationId` — it is `Stringable` — turned itself
* into text before this constructor looked: an identity built elsewhere, its ends already taken off.
* Anything but a text or `null` is a `TypeError` for every caller.
*/
public readonly ?string $rollbackContract = null,
mixed $rollbackContract = null,
/**
* Arguments that LOWER this ceiling for one call — the dangerous direction (decisions/0029).
*
Expand All @@ -91,6 +103,11 @@ public function __construct(
*/
public readonly array $descents = [],
) {
if ($rollbackContract !== null && !\is_string($rollbackContract)) {
throw new \TypeError(\sprintf('the rollback contract of an effect profile is given as text; got %s', get_debug_type($rollbackContract)));
}
$this->rollbackContract = $rollbackContract;

// A READ HAS NO SUBJECT, AND SAYING OTHERWISE IS IMPOSSIBLE RATHER THAN MERELY WRONG.
//
// Four blind judges classified thirty-three operations from the definition alone, and every
Expand Down Expand Up @@ -151,12 +168,12 @@ public function __construct(
// one profile, and it is asked where the table exists.
if ($reversibility === Reversibility::Guaranteed && !self::namesAnOperation((string) $rollbackContract)) {
throw new \InvalidArgumentException(\sprintf(
'the rollback contract «%s» describes an inverse instead of naming one: reversibility '
'the rollback contract %s describes an inverse instead of naming one: reversibility '
. '«guaranteed» must name the operation that undoes this (e.g. «plugins.disable»), '
. 'because prose cannot be run, cannot be checked, and cannot be put through a gate. '
. 'An operation whose inverse is not an operation is not guaranteed — declare what it '
. 'really is.',
$rollbackContract,
Canonical::shown($rollbackContract),
));
}
}
Expand All @@ -170,7 +187,7 @@ public function __construct(
*/
private static function namesAnOperation(string $contract): bool
{
return preg_match('/^[a-z0-9]+(?:[-][a-z0-9]+)*(?:\.[a-z0-9]+(?:[-][a-z0-9]+)*)*$/', OperationId::canonizar($contract)) === 1;
return preg_match('/^[a-z0-9]+(?:[-][a-z0-9]+)*(?:\.[a-z0-9]+(?:[-][a-z0-9]+)*)*$/D', OperationId::canonizar($contract)) === 1;
}

/**
Expand Down Expand Up @@ -391,8 +408,8 @@ public static function fromPartial(array $axes): self
foreach (array_keys($axes) as $key) {
if (!\in_array($key, $known, true)) {
throw new \InvalidArgumentException(sprintf(
'«%s» is not an effect axis (%s): changing it is a new proposal, not a tightening — use a counter',
(string) $key,
'%s is not an effect axis (%s): changing it is a new proposal, not a tightening — use a counter',
Canonical::shown((string) $key),
implode(', ', $known),
));
}
Expand All @@ -405,7 +422,7 @@ public static function fromPartial(array $axes): self
$raw = $axes[$axis];
$case = \is_string($raw) ? $enum::tryFrom($raw) : null;
if ($case === null) {
throw new \InvalidArgumentException(sprintf('%s is not a level of %s', json_encode($raw) ?: get_debug_type($raw), $axis));
throw new \InvalidArgumentException(sprintf('%s is not a level of %s', Canonical::shown($raw), $axis));
}

return $case;
Expand Down Expand Up @@ -456,7 +473,7 @@ public static function fromArray(array $data): self
$raw = $data[$key] ?? null;
$case = \is_string($raw) ? $enum::tryFrom($raw) : null;
if ($case === null) {
throw new \InvalidArgumentException(sprintf('a stored profile needs a valid «%s»; got %s', $key, json_encode($raw) ?: get_debug_type($raw)));
throw new \InvalidArgumentException(sprintf('a stored profile needs a valid «%s»; got %s', $key, Canonical::shown($raw)));
}

return $case;
Expand Down
2 changes: 1 addition & 1 deletion src/Operation.php
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ public function instrumentDigest(): ?string

$effects = $this->effectCeiling();
$scopes = $this->scopes;
sort($scopes);
sort($scopes, \SORT_STRING);

return 'sha256:' . hash('sha256', Standing\Canonical::json([
'acts_on' => $subject,
Expand Down
Loading
Loading